You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
// frontend/components/settings/AccountTab.tsx — 프로필(이름·역할) 편집 + 로그인 정보
|
|
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { Avatar } from "@/components/Avatar";
|
|
import { Button } from "@/components/Button";
|
|
import { Icon } from "@/components/Icon";
|
|
import { logout } from "@/lib/auth";
|
|
import { settingsApi } from "@/lib/settings/api";
|
|
import { useMe } from "@/lib/hooks/useSettings";
|
|
import type { ToastFn } from "./SettingsClient";
|
|
|
|
const AUTH_ENABLED = process.env.NEXT_PUBLIC_AUTH_ENABLED === "true";
|
|
|
|
export function AccountTab({ toast }: { toast: ToastFn }) {
|
|
const { data: me, isLoading, refresh } = useMe();
|
|
const [name, setName] = useState("");
|
|
const [role, setRole] = useState("");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (me) {
|
|
setName(me.name);
|
|
setRole(me.role === "member" || me.role === "admin" ? me.role : me.role || "");
|
|
}
|
|
}, [me]);
|
|
|
|
const dirty = me ? name.trim() !== me.name || role.trim() !== (me.role ?? "") : false;
|
|
|
|
const onSave = async () => {
|
|
if (!name.trim()) {
|
|
toast("이름을 입력해 주세요", "amber");
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
try {
|
|
await settingsApi.updateProfile({ name: name.trim(), role: role.trim() });
|
|
await refresh();
|
|
toast("프로필을 저장했어요", "green");
|
|
} catch {
|
|
toast("저장에 실패했어요", "coral");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const onLogout = async () => {
|
|
await logout();
|
|
if (typeof window !== "undefined") window.location.assign("/login");
|
|
};
|
|
|
|
return (
|
|
<section className="set-sec" aria-label="계정">
|
|
<header className="set-sechead">
|
|
<h2>계정</h2>
|
|
<p>이름과 역할은 대시보드·아바타에 표시돼요.</p>
|
|
</header>
|
|
|
|
<div className="set-card set-profile">
|
|
<Avatar initial={me?.initial || "지"} tone="blue" label={me?.name} />
|
|
<div className="sp-meta">
|
|
<b>{me?.name ?? (isLoading ? "불러오는 중…" : "지우")}</b>
|
|
<span className="sp-email">
|
|
<Icon name="mail" />
|
|
{me?.email || "이메일 미설정"}
|
|
</span>
|
|
</div>
|
|
<span className="sp-role" data-role={me?.role}>
|
|
{me?.role === "admin" ? "관리자" : me?.role || "멤버"}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="set-card">
|
|
<div className="set-field">
|
|
<label htmlFor="acc-name">이름</label>
|
|
<input
|
|
id="acc-name"
|
|
className="set-input"
|
|
value={name}
|
|
maxLength={40}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="표시 이름"
|
|
/>
|
|
</div>
|
|
<div className="set-field">
|
|
<label htmlFor="acc-role">역할 / 직함</label>
|
|
<input
|
|
id="acc-role"
|
|
className="set-input"
|
|
value={role}
|
|
maxLength={40}
|
|
onChange={(e) => setRole(e.target.value)}
|
|
placeholder="예: 프로덕트 디자이너"
|
|
/>
|
|
</div>
|
|
<div className="set-actions">
|
|
<Button variant="lime" icon="tick" onClick={onSave} disabled={!dirty || saving}>
|
|
{saving ? "저장 중…" : "변경사항 저장"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="set-card set-row-between">
|
|
<div className="sp-meta">
|
|
<b>로그인 세션</b>
|
|
<span className="set-hint">
|
|
{AUTH_ENABLED
|
|
? "현재 기기에서 로그인되어 있어요."
|
|
: "데모 모드 — 인증이 꺼져 있어요(AUTH_ENABLED=false)."}
|
|
</span>
|
|
</div>
|
|
{AUTH_ENABLED && (
|
|
<Button variant="glass" icon="back" onClick={onLogout}>
|
|
로그아웃
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|