i18n 적용
This commit is contained in:
+47
-45
@@ -15,6 +15,9 @@ import {
|
||||
FolderKanban,
|
||||
SunMoon,
|
||||
} from "lucide-react";
|
||||
import { useAppLocale } from "@/features/i18n/LocaleProvider";
|
||||
import { getDashboardMessages } from "@/features/i18n/messages/dashboard";
|
||||
import { getProjectsMessages } from "@/features/i18n/messages/projects";
|
||||
|
||||
interface ProjectListItem {
|
||||
id: string;
|
||||
@@ -27,6 +30,9 @@ interface ProjectListItem {
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const router = useRouter();
|
||||
const locale = useAppLocale();
|
||||
const t = getDashboardMessages(locale);
|
||||
const p = getProjectsMessages(locale);
|
||||
const [projects, setProjects] = useState<ProjectListItem[]>([]);
|
||||
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
@@ -49,12 +55,10 @@ export default function ProjectsPage() {
|
||||
setStatus("error");
|
||||
|
||||
if (res.status === 401) {
|
||||
setErrorMessage("프로젝트 목록을 불러오는 중 인증 오류가 발생했습니다. 다시 로그인해 주세요.");
|
||||
setErrorMessage(p.errorFetchUnauthorized);
|
||||
router.push("/login");
|
||||
} else {
|
||||
setErrorMessage(
|
||||
"프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
|
||||
);
|
||||
setErrorMessage(p.errorFetchGeneric);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -69,7 +73,7 @@ export default function ProjectsPage() {
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setStatus("error");
|
||||
setErrorMessage("프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
||||
setErrorMessage(p.errorFetchGeneric);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -88,9 +92,7 @@ export default function ProjectsPage() {
|
||||
|
||||
const handleDelete = async (slug: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const confirmed = window.confirm(
|
||||
"이 프로젝트를 삭제할까요? 삭제 후에는 되돌릴 수 없습니다. (로컬 저장 및 자동저장 데이터도 함께 삭제됩니다.)",
|
||||
);
|
||||
const confirmed = window.confirm(p.confirmDeleteSingle);
|
||||
|
||||
if (!confirmed) return;
|
||||
}
|
||||
@@ -102,7 +104,7 @@ export default function ProjectsPage() {
|
||||
|
||||
if (!res.ok) {
|
||||
setStatus("error");
|
||||
setErrorMessage("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
||||
setErrorMessage(p.errorDeleteSingle);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +120,7 @@ export default function ProjectsPage() {
|
||||
setErrorMessage(null);
|
||||
} catch {
|
||||
setStatus("error");
|
||||
setErrorMessage("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
||||
setErrorMessage(p.errorDeleteSingle);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,9 +154,7 @@ export default function ProjectsPage() {
|
||||
if (slugs.length === 0) return;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const confirmed = window.confirm(
|
||||
"선택한 프로젝트를 모두 삭제할까요? 삭제 후에는 되돌릴 수 없습니다. (로컬 저장 및 자동저장 데이터도 함께 삭제됩니다.)",
|
||||
);
|
||||
const confirmed = window.confirm(p.confirmDeleteBulk);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export default function ProjectsPage() {
|
||||
|
||||
if (okSlugs.length === 0) {
|
||||
setStatus("error");
|
||||
setErrorMessage("선택한 프로젝트를 삭제하는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
||||
setErrorMessage(p.errorDeleteBulkAllFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -186,16 +186,14 @@ export default function ProjectsPage() {
|
||||
|
||||
if (okSlugs.length !== slugs.length) {
|
||||
setStatus("error");
|
||||
setErrorMessage(
|
||||
"일부 프로젝트 삭제에 실패했습니다. 페이지를 새로고침한 뒤 목록을 확인해 주세요.",
|
||||
);
|
||||
setErrorMessage(p.errorDeleteBulkSomeFailed);
|
||||
} else {
|
||||
setStatus("idle");
|
||||
setErrorMessage(null);
|
||||
}
|
||||
} catch {
|
||||
setStatus("error");
|
||||
setErrorMessage("선택한 프로젝트를 삭제하는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
||||
setErrorMessage(p.errorDeleteBulkAllFailed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -216,8 +214,8 @@ export default function ProjectsPage() {
|
||||
<main className="min-h-screen flex flex-col bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
|
||||
<header className="border-b border-slate-200 px-6 py-4 flex items-center justify-between bg-white/80 backdrop-blur dark:border-slate-800 dark:bg-slate-950/80">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">프로젝트 목록</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">저장된 프로젝트들을 한 눈에 볼 수 있는 목록</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{p.headerTitle}</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{p.headerDescription}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<nav className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white/80 px-1.5 py-1 shadow-sm dark:border-slate-700 dark:bg-slate-900/80">
|
||||
@@ -226,7 +224,7 @@ export default function ProjectsPage() {
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-sky-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:hover:text-sky-200"
|
||||
>
|
||||
<LayoutDashboard className="w-4 h-4" aria-hidden="true" />
|
||||
<span>대시보드</span>
|
||||
<span>{t.navDashboard}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/projects"
|
||||
@@ -234,14 +232,14 @@ export default function ProjectsPage() {
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-semibold bg-sky-600 text-white shadow-sm hover:bg-sky-700"
|
||||
>
|
||||
<FolderKanban className="w-4 h-4" aria-hidden="true" />
|
||||
<span>프로젝트 목록</span>
|
||||
<span>{t.navProjects}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/projects/submissions"
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-emerald-700 hover:bg-emerald-50 hover:text-emerald-800 dark:text-emerald-200 dark:hover:bg-emerald-900/60 dark:hover:text-emerald-100"
|
||||
>
|
||||
<ListChecks className="w-4 h-4" aria-hidden="true" />
|
||||
<span>전체 제출 내역</span>
|
||||
<span>{t.navSubmissions}</span>
|
||||
</Link>
|
||||
</nav>
|
||||
<button
|
||||
@@ -250,7 +248,7 @@ export default function ProjectsPage() {
|
||||
onClick={handleToggleTheme}
|
||||
>
|
||||
<SunMoon className="w-4 h-4" aria-hidden="true" />
|
||||
<span>테마 전환</span>
|
||||
<span>{t.themeToggleLabel}</span>
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
@@ -260,7 +258,7 @@ export default function ProjectsPage() {
|
||||
setIsMenuOpen((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
메뉴
|
||||
{t.menuLabel}
|
||||
</button>
|
||||
{isMenuOpen && (
|
||||
<div className="absolute right-0 mt-1 w-32 rounded border border-slate-200 bg-white shadow-lg z-20 dark:border-slate-700 dark:bg-slate-900/95">
|
||||
@@ -272,7 +270,7 @@ export default function ProjectsPage() {
|
||||
void handleLogout();
|
||||
}}
|
||||
>
|
||||
로그아웃
|
||||
{t.logoutLabel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -282,7 +280,7 @@ export default function ProjectsPage() {
|
||||
<section className="flex-1 px-6 py-4 overflow-auto bg-slate-50/60 dark:bg-transparent">
|
||||
{status === "error" && (
|
||||
<p className="text-xs text-red-500 mb-3 dark:text-red-300">
|
||||
{errorMessage ?? "프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."}
|
||||
{errorMessage ?? p.errorFetchGeneric}
|
||||
</p>
|
||||
)}
|
||||
{projects.length === 0 && status === "idle" && (
|
||||
@@ -298,7 +296,9 @@ export default function ProjectsPage() {
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full border bg-slate-100 border-slate-300 text-slate-700 dark:bg-slate-900/70 dark:border-slate-700 dark:text-slate-200">
|
||||
<ListChecks className="w-3 h-3 text-slate-400 dark:text-slate-500" aria-hidden="true" />
|
||||
<span>
|
||||
선택된 프로젝트: <span className="font-semibold">{selectedSlugs.length}</span>개
|
||||
{p.toolbarSelectedPrefix}
|
||||
<span className="font-semibold">{selectedSlugs.length}</span>
|
||||
{p.toolbarSelectedSuffix}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -312,14 +312,14 @@ export default function ProjectsPage() {
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" aria-hidden="true" />
|
||||
<span>선택 삭제</span>
|
||||
<span>{p.toolbarDeleteSelected}</span>
|
||||
</button>
|
||||
<Link
|
||||
href="/editor?new=1"
|
||||
className="inline-flex items-center gap-1 rounded border px-2 py-1 text-xs font-medium border-sky-500 bg-sky-600 text-white hover:bg-sky-700 shadow-sm dark:border-sky-700 dark:bg-sky-950 dark:text-sky-100 dark:hover:bg-sky-900"
|
||||
>
|
||||
<FilePlus2 className="w-3 h-3" aria-hidden="true" />
|
||||
<span>새 프로젝트 만들기</span>
|
||||
<span>{p.toolbarCreateNew}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,7 +330,7 @@ export default function ProjectsPage() {
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3 rounded border-slate-700 bg-slate-900 align-middle"
|
||||
aria-label="전체 선택"
|
||||
aria-label={p.tableHeaderSelectAllAria}
|
||||
checked={
|
||||
pageProjects.length > 0 && pageProjects.every((p) => selectedSlugs.includes(p.slug))
|
||||
}
|
||||
@@ -345,12 +345,12 @@ export default function ProjectsPage() {
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th className="py-2 pr-4">제목</th>
|
||||
<th className="py-2 pr-4">주소(slug)</th>
|
||||
<th className="py-2 pr-4">상태</th>
|
||||
<th className="py-2 pr-4">생성일</th>
|
||||
<th className="py-2 pr-4">수정일</th>
|
||||
<th className="py-2 pr-4 text-right">액션</th>
|
||||
<th className="py-2 pr-4">{p.tableHeaderTitle}</th>
|
||||
<th className="py-2 pr-4">{p.tableHeaderSlug}</th>
|
||||
<th className="py-2 pr-4">{p.tableHeaderStatus}</th>
|
||||
<th className="py-2 pr-4">{p.tableHeaderCreatedAt}</th>
|
||||
<th className="py-2 pr-4">{p.tableHeaderUpdatedAt}</th>
|
||||
<th className="py-2 pr-4 text-right">{p.tableHeaderActions}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -365,7 +365,7 @@ export default function ProjectsPage() {
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-3 w-3 rounded border-slate-700 bg-slate-900 align-middle"
|
||||
aria-label={`${project.title} 선택`}
|
||||
aria-label={`${project.title}${p.tableRowSelectAriaSuffix}`}
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
@@ -394,21 +394,21 @@ export default function ProjectsPage() {
|
||||
className="inline-flex items-center gap-1 text-sky-600 hover:text-sky-700 underline-offset-2 hover:underline dark:text-sky-300 dark:hover:text-sky-200"
|
||||
>
|
||||
<Pencil className="w-3 h-3" aria-hidden="true" />
|
||||
<span>편집</span>
|
||||
<span>{p.actionEdit}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/preview?slug=${encodeURIComponent(project.slug)}`}
|
||||
className="inline-flex items-center gap-1 text-slate-600 hover:text-slate-800 underline-offset-2 hover:underline dark:text-slate-300 dark:hover:text-slate-100"
|
||||
>
|
||||
<Eye className="w-3 h-3" aria-hidden="true" />
|
||||
<span>미리보기</span>
|
||||
<span>{p.actionPreview}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/projects/${encodeURIComponent(project.slug)}/submissions`}
|
||||
className="inline-flex items-center gap-1 text-emerald-600 hover:text-emerald-700 underline-offset-2 hover:underline dark:text-emerald-300 dark:hover:text-emerald-100"
|
||||
>
|
||||
<ListChecks className="w-3 h-3" aria-hidden="true" />
|
||||
<span>폼 제출 내역</span>
|
||||
<span>{p.actionViewSubmissions}</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
@@ -418,7 +418,7 @@ export default function ProjectsPage() {
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" aria-hidden="true" />
|
||||
<span>삭제</span>
|
||||
<span>{p.actionDelete}</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -431,7 +431,9 @@ export default function ProjectsPage() {
|
||||
<div className="mt-3 flex items-center justify-between text-[11px] text-slate-600 dark:text-slate-300">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<span className="px-2 py-1 rounded-full bg-slate-100 border border-slate-300 text-slate-700 dark:bg-slate-900/70 dark:border-slate-700 dark:text-slate-200">
|
||||
전체 {totalCount}개 중{" "}
|
||||
{p.pagerTotalPrefix}
|
||||
{totalCount}
|
||||
{p.pagerTotalCountSuffix}
|
||||
<span className="font-semibold">
|
||||
{totalCount === 0 ? 0 : startIndex + 1}–{Math.min(endIndex, totalCount)}
|
||||
</span>
|
||||
@@ -465,7 +467,7 @@ export default function ProjectsPage() {
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
<ChevronLeft className="w-3 h-3" aria-hidden="true" />
|
||||
<span>이전</span>
|
||||
<span>{p.pagerPrevLabel}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -473,7 +475,7 @@ export default function ProjectsPage() {
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-slate-300 text-slate-700 bg-white disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:bg-slate-900 dark:hover:bg-slate-800"
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
>
|
||||
<span>다음</span>
|
||||
<span>{p.pagerNextLabel}</span>
|
||||
<ChevronRight className="w-3 h-3" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user