/* ============================================================================ * 서명전에 — iOS History & Account tabs * ========================================================================== */ function HistoryScreen({ onOpen, onDelete, items: itemsProp, historyLoading = false, hasSession = false }) { const [scrolled, setScrolled] = useState(false); const [editing, setEditing] = useState(false); const [selected, setSelected] = useState(() => new Set()); const [busy, setBusy] = useState(false); const dialog = (typeof window !== "undefined" && typeof window.useDialog === "function") ? window.useDialog() : null; const toast = (typeof window !== "undefined" && typeof window.useToast === "function") ? window.useToast() : null; const MOCK_ITEMS = [ { id: "r1", type: "jeonse", title: "전세계약서_202604.pdf", sev: "alert", high: 3, when: "오늘 오후 2:32" }, { id: "r3", type: "wolsae", title: "월세계약서_202603.pdf", sev: "warn", high: 2, when: "3월 30일" }, { id: "r4", type: "jeonse", title: "원룸전세_구로동.pdf", sev: "safe", high: 0, when: "3월 12일" }, { id: "r6", type: "wolsae", title: "투룸월세_역삼동.pdf", sev: "warn", high: 1, when: "2월 28일" }, ]; const hasReal = Array.isArray(itemsProp) && itemsProp.length > 0; const items = hasReal ? itemsProp : MOCK_ITEMS; const showEmpty = hasSession && !historyLoading && Array.isArray(itemsProp) && itemsProp.length === 0; // 목업이 아닌 실제 데이터에서만 편집 모드 활성화 (목업 삭제는 무의미) const canEdit = hasReal && typeof onDelete === "function"; // 항목이 모두 사라지면 자동으로 편집 모드 종료 useEffect(() => { if (editing && (!hasReal || items.length === 0)) { setEditing(false); setSelected(new Set()); } }, [editing, hasReal, items.length]); const groups = { "오늘": items.filter((i) => i.when.includes("오늘")), "어제": items.filter((i) => i.when === "어제"), "이전": items.filter((i) => !i.when.includes("오늘") && i.when !== "어제"), }; const thumbClass = (sev) => "recent__thumb recent__thumb--" + sev; function toggleSelect(id) { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function exitEdit() { setEditing(false); setSelected(new Set()); } async function handleDeleteSelected() { if (busy) return; if (selected.size === 0) return; const targets = items.filter((it) => selected.has(it.id)); if (targets.length === 0) return; // 확인 다이얼로그 — useDialog 가 안 떠 있으면 window.confirm 으로 폴백 let ok = false; if (dialog?.openAsyncConfirm) { ok = await dialog.openAsyncConfirm({ title: `선택한 ${targets.length}개 기록을 지울까요?`, description: "한번 지우면 되돌릴 수 없어요. 분석 결과와 PDF 도 같이 지워져요.", confirmLabel: "지우기", cancelLabel: "그대로 두기", }); } else if (typeof window !== "undefined" && typeof window.confirm === "function") { ok = window.confirm(`선택한 ${targets.length}개 기록을 지울까요? 되돌릴 수 없어요.`); } else { ok = true; } if (!ok) return; setBusy(true); try { const results = await Promise.all( targets.map((t) => Promise.resolve(onDelete(t)).catch((err) => ({ ok: false, error: err })), ), ); const failed = results.filter((r) => r && r.ok === false).length; const succeeded = results.length - failed; if (succeeded > 0 && toast?.openToast) { toast.openToast({ description: `${succeeded}개 기록을 지웠어요` }); } if (failed > 0 && toast?.openToast) { toast.openToast({ description: `${failed}개는 지우지 못했어요. 잠시 뒤 다시 시도해 주세요.` }); } // 성공이든 실패든 선택은 비우고, 모두 성공하면 편집 모드 종료 setSelected(new Set()); if (failed === 0) setEditing(false); } finally { setBusy(false); } } return (
{editing ? ( ) : canEdit ? ( ) : ( )}
{editing ? (selected.size > 0 ? `${selected.size}개 선택됨` : "기록 편집") : "기록"}
{editing ? ( ) : ( )}
setScrolled(e.currentTarget.scrollTop > 20)}>

{editing ? (selected.size > 0 ? `${selected.size}개 선택됨` : "지울 기록 고르기") : "기록"}

{!editing && (
)} {showEmpty && (
아직 분석한 계약서가 없어요
홈 화면에서 계약서를 올려 보세요.
)} {!showEmpty && Object.entries(groups).map(([key, list]) => list.length > 0 && (
{key}
{list.map((r, i) => { const isSelected = editing && selected.has(r.id); return ( ); })}
))} 0 ? "120px" : "24px"} />
{/* 편집 모드 하단 삭제 CTA — 선택이 있을 때만 노출 */} {editing && selected.size > 0 && (
)}
); } function AccountScreen({ credits, unlimited = false, onOpenBilling, onOpenHelp, onToggleTheme, theme, userEmail, onLogout, authMode = "toss-anonymous" }) { // toss-anonymous 모드에서는 email 이 없어요. 사용자 이름/아바타/로그아웃 버튼의 // 디폴트 카피를 "토스 자동 로그인" 정합성에 맞게 바꿔주는 게 핵심. // 계정 화면 행(row) 정책 메모 (G-3 후속 정리, 2026-04 김인아 피드백 반영) // - 결제 수단 / 결제 내역: 토스 미니앱은 카드/결제수단 등록을 토스 플랫폼이 직접 // 관리하기 때문에 앱 안에서 등록 화면으로 보낼 수 없다. 잘못된 기대를 만들지 // 않도록 두 행 모두 제거하고, "크레딧 충전하기" 버튼 하나로 결제 흐름을 모았다. // - 알림: 푸시/인앱 알림 시스템이 아직 없어서 "켜져 있어요" 라는 표시가 거짓말. // 기능이 들어오기 전까진 행 자체를 노출하지 않는다. // - 개인정보와 보안: 별도 화면이 없고, 같은 내용이 도움말·이용 약관에서 다뤄진다. // 중복이라 제거. // - 도움말과 문의: 하단 탭의 "도움말" 화면으로 이동시킨다 (onOpenHelp). // - 이용 약관과 개인정보 처리방침: window.openLegalSheet("terms") 그대로 유지. const [scrolled, setScrolled] = useState(false); const isTossMode = authMode === "toss-anonymous"; const localPart = userEmail ? userEmail.split("@")[0] : ""; // 토스 모드: 이메일이 없는 게 정상. 사용자 입장에선 "이미 로그인됨" 상태이므로 // "로그인 안 했어요" 같은 카피는 절대 노출하면 안 돼요. const displayName = userEmail ? localPart : (isTossMode ? "토스로 자동 로그인됐어요" : "로그인하고 시작해 보세요"); const displayEmail = userEmail ? userEmail : (isTossMode ? "별도 가입 없이 바로 사용 중이에요" : "아직 로그인 안 했어요"); const avatarChar = userEmail ? (localPart[0] || "?").toUpperCase() : (isTossMode ? "T" : "?"); return (
계정
setScrolled(e.currentTarget.scrollTop > 20)}>

계정

{avatarChar}
{displayName}
{displayEmail}
{/* 크레딧 지갑 카드 — 상단에 바로 노출 */}
{unlimited ? "분석 크레딧" : "남은 분석 크레딧"}
{unlimited ? ( <> 무제한 ) : ( <> {credits} )}
{unlimited ? ( 베타 기간 동안 무제한으로 쓸 수 있어요 ) : ( 만료 기한 없어요 )}
{!unlimited && ( )}
{/* 결제 수단 / 결제 내역 / 알림 / 개인정보와 보안 행은 위 정책 메모 참고 — 모두 제거. 크레딧 충전은 위쪽 "크레딧 충전하기" 버튼으로 일원화. */}
{/* 로그아웃 버튼 정책 - toss-anonymous : 진짜 "로그아웃"은 불가(SDK가 같은 hash를 다시 줌). 대신 "내 데이터 초기화"로 라벨을 바꿔서 의미를 명확히 함. signOut() 가 web-fallback hash 캐시를 비우고 새 hash 로 재부트스트랩 → 로컬 캐시/스토어가 깨끗해진 상태로 다시 진입. - supabase-jwt : 기존 그대로 — userEmail 유무로 로그인/로그아웃 토글. */} {isTossMode ? ( ) : ( )}
올려 주신 계약서 원본은 분석이 끝나면 바로 지워요. 서버에는 남기지 않아요.
서명전에 v1.3 · Build 2026.04
); } /* iOS style toggle switch */ function IOSToggle({ on, onChange }) { return ( { e.stopPropagation(); onChange?.(); }} style={{ width: 51, height: 31, borderRadius: 31, background: on ? "var(--safe)" : "var(--fill-1)", position: "relative", transition: "background 200ms", cursor: "pointer", flexShrink: 0, }} > ); } window.HistoryScreen = HistoryScreen; window.AccountScreen = AccountScreen; window.IOSToggle = IOSToggle; Object.assign(window, { HistoryScreen, AccountScreen });