diff --git a/src/lib/i18n/ko.json b/src/lib/i18n/ko.json index f1e4cc3..cf3c9df 100644 --- a/src/lib/i18n/ko.json +++ b/src/lib/i18n/ko.json @@ -7,6 +7,34 @@ "logout": "로그아웃", "admin": "관리자" }, + "common": { + "name": "이름", + "code": "코드", + "description": "설명", + "status": "상태", + "actions": "작업", + "created": "생성", + "add": "추가", + "cancel": "취소", + "save": "저장", + "edit": "수정", + "delete": "삭제", + "confirm_delete": "삭제하시겠습니까?", + "none": "없음", + "all": "전체", + "apply": "적용", + "reset": "초기화", + "collapse": "접기", + "expand": "편집", + "status_active": "활성", + "status_inactive": "비활성", + "status_disabled": "비활성", + "status_locked": "잠김", + "primary_member": "주소속", + "remove": "제거", + "change": "변경", + "saving": "저장되었습니다." + }, "login": { "username": "아이디", "password": "비밀번호", @@ -32,5 +60,298 @@ "profile": "내 프로필", "mfa": "2단계 인증", "passkeys": "패스키" + }, + "logout": { + "in_progress": "로그아웃 중..." + }, + "mfa_login": { + "title": "2단계 인증", + "backup_code_hint": "백업 코드를 입력해 주세요.", + "totp_hint": "인증 앱의 6자리 코드를 입력해 주세요.", + "backup_code_label": "백업 코드", + "totp_code_label": "인증 코드", + "confirm": "확인", + "use_backup": "백업 코드로 로그인", + "use_totp": "인증 앱 코드로 로그인", + "back_to_login": "← 로그인으로 돌아가기" + }, + "mfa_manage": { + "title": "2단계 인증 관리", + "back_to_home": "← 홈으로", + "backup_codes_generated": "백업 코드가 생성되었습니다", + "backup_codes_warning": "이 코드는 지금만 표시됩니다. 안전한 곳에 저장해 두세요. 코드 하나당 1회만 사용 가능합니다.", + "setup_title": "인증 앱 등록", + "setup_hint": "Google Authenticator, Authy 등 TOTP 앱으로 QR 코드를 스캔하세요.", + "qr_loading": "로딩 중...", + "qr_error": "QR 코드 생성 실패", + "manual_entry": "직접 입력 (키 보기)", + "code_input_label": "앱에 표시된 6자리 코드 입력", + "register_complete": "등록 완료", + "new_qr": "새 QR 코드 생성", + "enrolled_badge": "TOTP 인증기가 등록되어 있습니다.", + "enrolled_date": "등록일:", + "backup_codes_label": "백업 코드", + "backup_codes_remaining": "{{count}}개 남음", + "backup_codes_low": "— 코드가 부족합니다", + "backup_codes_empty": "— 재생성이 필요합니다", + "regenerate": "재생성", + "regenerate_confirm": "기존 백업 코드가 모두 삭제되고 새로 발급됩니다. 계속하시겠습니까?", + "totp_placeholder": "TOTP 코드", + "delete_input_placeholder": "현재 TOTP 코드 입력 후 삭제", + "delete_button": "인증기 삭제", + "delete_confirm": "TOTP 인증기와 모든 백업 코드가 삭제됩니다. 계속하시겠습니까?", + "not_enrolled_hint": "2단계 인증을 활성화하면 계정 보안이 강화됩니다. Google Authenticator, Authy 등의 인증 앱이 필요합니다.", + "start_setup": "인증기 등록 시작" + }, + "passkeys": { + "title": "패스키 관리", + "subtitle": "지문, Face ID, 보안 키 등으로 비밀번호 없이 로그인할 수 있습니다.", + "name_label": "패스키 이름 (선택)", + "registered_date": "등록:", + "last_used": "· 마지막 사용:", + "delete_confirm": "이 패스키를 삭제하시겠습니까?", + "empty": "등록된 패스키가 없습니다.", + "registering": "등록 중...", + "register": "패스키 등록" + }, + "profile": { + "title": "내 프로필", + "saved": "프로필이 저장되었습니다.", + "basic_info": "기본 정보", + "given_name": "이름 (Given Name)", + "family_name": "성 (Family Name)", + "display_name": "표시 이름", + "birthdate": "생년월일", + "phone": "전화번호", + "bio": "소개", + "locale": "언어", + "timezone": "시간대", + "locale_settings": "지역 설정", + "org_membership": "조직 소속", + "team_label": "팀", + "part_label": "파트" + }, + "audit": { + "title": "감사 로그", + "showing": "{{count}}건 표시 (페이지 {{pageSize}}건)", + "event_kind": "이벤트 종류", + "outcome": "결과", + "success": "성공", + "failure": "실패", + "col_time": "시각", + "col_user": "사용자", + "col_event": "이벤트", + "col_ip": "IP", + "col_detail": "상세", + "empty": "감사 로그가 없습니다.", + "next_page": "다음 페이지 →" + }, + "departments": { + "title": "부서 관리", + "add_btn": "+ 부서 추가", + "create_title": "새 부서 추가", + "name_label": "부서명 *", + "parent_label": "상위 부서", + "parent_none": "없음 (최상위)", + "display_order": "표시 순서", + "col_name": "부서명", + "col_parent": "상위 부서", + "empty": "등록된 부서가 없습니다.", + "delete_confirm": "삭제하시겠습니까? 하위 부서의 상위 부서 연결이 해제됩니다." + }, + "positions": { + "title": "직급 관리", + "add_btn": "+ 직급 추가", + "create_title": "새 직급 추가", + "name_label": "직급명 *", + "level_label": "레벨 (높을수록 고위)", + "col_name": "직급명", + "col_level": "레벨", + "empty": "등록된 직급이 없습니다." + }, + "teams": { + "title": "팀 관리", + "add_btn": "+ 팀 추가", + "create_title": "새 팀 추가", + "name_label": "팀명 *", + "dept_label": "소속 부서", + "dept_none": "없음 (독립 팀)", + "col_name": "팀명", + "col_dept": "소속 부서", + "empty": "등록된 팀이 없습니다." + }, + "parts": { + "title": "파트 관리", + "add_btn": "+ 파트 추가", + "create_title": "새 파트 추가", + "name_label": "파트명 *", + "team_label": "소속 팀", + "team_none": "없음 (독립 파트)", + "col_name": "파트명", + "col_team": "소속 팀", + "col_dept": "부서", + "empty": "등록된 파트가 없습니다." + }, + "signing_keys": { + "title": "서명 키 관리", + "rotate_btn": "키 로테이션", + "rotate_confirm": "새 키를 생성하고 기존 활성 키를 비활성화합니다.\n계속하시겠습니까?", + "rotated_title": "키 로테이션이 완료되었습니다", + "new_kid_label": "새 KID:", + "col_alg": "알고리즘", + "col_use": "용도", + "col_cert": "인증서", + "col_created": "생성일", + "col_rotated": "로테이션", + "col_expires": "만료일", + "cert_yes": "있음", + "cert_no": "없음", + "empty": "등록된 서명 키가 없습니다. 키 로테이션 버튼을 눌러 첫 번째 키를 생성하세요." + }, + "users": { + "title": "사용자 관리", + "add_btn": "+ 사용자 추가", + "create_title": "새 사용자 추가", + "email_label": "이메일 *", + "username_label": "아이디 (미입력 시 자동)", + "display_name_label": "이름", + "role_label": "역할 *", + "password_label": "비밀번호 * (8자 이상)", + "role_user": "일반 사용자", + "role_admin": "관리자", + "role_user_short": "일반", + "status_active": "활성", + "status_disabled": "비활성", + "status_locked": "잠김", + "action_disable": "비활성화", + "action_enable": "활성화", + "action_unlock": "잠금 해제", + "reset_password": "비밀번호 초기화", + "new_password_placeholder": "새 비밀번호 (8자 이상)", + "col_id_email": "아이디 / 이메일", + "col_name": "이름", + "col_role": "역할", + "empty": "등록된 사용자가 없습니다.", + "delete_confirm": "사용자를 삭제하시겠습니까?" + }, + "user_detail": { + "back": "← 목록", + "saved": "저장되었습니다.", + "profile_section": "프로필 정보", + "given_name": "이름 (Given Name)", + "family_name": "성 (Family Name)", + "display_name": "표시 이름", + "phone": "전화번호", + "birthdate": "생년월일", + "bio": "소개", + "locale": "언어", + "timezone": "시간대", + "role": "역할", + "status": "상태", + "role_user": "일반 사용자", + "role_admin": "관리자", + "status_active": "활성", + "status_disabled": "비활성", + "status_locked": "잠김", + "dept_section": "부서 소속", + "dept_empty": "소속된 부서가 없습니다.", + "dept_select": "부서 선택", + "position_none": "직급 없음", + "job_title_placeholder": "직책 (예: 팀장)", + "primary": "주소속", + "team_section": "팀 소속", + "team_empty": "소속된 팀이 없습니다.", + "team_select": "팀 선택", + "part_section": "파트 소속", + "part_empty": "소속된 파트가 없습니다.", + "part_select": "파트 선택", + "part_job_title_placeholder": "직책 (예: 파트장)" + }, + "ldap": { + "title": "LDAP 프로바이더", + "add_btn": "+ 프로바이더 추가", + "create_title": "새 LDAP 프로바이더", + "added_success": "LDAP 프로바이더가 추가되었습니다. 테스트 후 활성화하세요.", + "saved": "저장되었습니다.", + "name_label": "이름 *", + "tls_label": "TLS 모드", + "tls_none": "없음 (ldap://)", + "tls_tls": "TLS (ldaps://)", + "tls_starttls": "STARTTLS", + "host_label": "호스트 *", + "port_label": "포트", + "base_dn_label": "Base DN", + "auth_section": "인증 방식 — Admin Bind + Search (ou가 여러 개인 경우) 또는 DN 패턴", + "auth_hint": "Admin Bind DN을 입력하면 Search 방식을 사용합니다. 비워두면 유저 DN 패턴을 사용합니다.", + "bind_dn_label": "Admin Bind DN", + "bind_pw_label": "Admin Bind 패스워드", + "search_filter_label": "유저 검색 필터", + "dn_pattern_label": "유저 DN 패턴", + "dn_pattern_hint": "(Admin Bind 미사용 시)", + "attr_section": "속성 매핑 (기본값: mail, cn, givenName, sn)", + "attr_email": "이메일 속성", + "attr_display": "표시 이름 속성", + "attr_given": "이름 속성", + "attr_family": "성 속성", + "enable": "활성화", + "col_name": "이름", + "col_host": "호스트", + "col_dn_pattern": "유저 DN 패턴", + "col_created": "생성", + "empty": "등록된 LDAP 프로바이더가 없습니다.", + "delete_confirm": "LDAP 프로바이더를 삭제하시겠습니까?" + }, + "oidc": { + "title": "OIDC 클라이언트", + "add_btn": "+ 클라이언트 추가", + "create_title": "새 OIDC 클라이언트", + "created_title": "클라이언트가 생성되었습니다", + "created_secret_hint": "시크릿은 지금만 표시됩니다. 안전한 곳에 저장해 두세요.", + "secret_regenerated_title": "시크릿이 재생성되었습니다", + "secret_regenerated_hint": "기존 시크릿은 더 이상 사용할 수 없습니다.", + "name_label": "이름 *", + "redirect_uris_label": "Redirect URIs * (줄바꿈 구분)", + "post_logout_label": "Post-Logout Redirect URIs (줄바꿈 구분)", + "auth_method_label": "인증 방식", + "auth_method_none": "none (공개 클라이언트)", + "pkce_required": "PKCE 필수", + "enabled": "활성", + "col_name_id": "이름 / Client ID", + "col_auth": "인증", + "col_created": "생성", + "empty": "등록된 클라이언트가 없습니다.", + "regenerate_secret": "시크릿 재생성", + "regenerate_confirm": "시크릿을 재생성하시겠습니까? 기존 시크릿은 즉시 무효화됩니다.", + "delete_confirm": "클라이언트를 삭제하시겠습니까?" + }, + "saml": { + "title": "SAML SP 관리", + "add_btn": "+ SP 추가", + "create_title": "새 SAML SP 등록", + "metadata_hint": "SP 메타데이터 XML로 자동 입력", + "metadata_optional": "(선택)", + "metadata_placeholder": " 를 붙여넣으세요", + "parse_btn": "메타데이터 파싱 →", + "name_label": "이름 *", + "entity_id_label": "Entity ID *", + "acs_url_label": "ACS URL *", + "slo_url_label": "SLO URL", + "name_id_label": "NameID Format", + "sign_assertion": "Assertion 서명", + "sign_response": "Response 서명", + "want_signed": "AuthnRequest 서명 요구", + "cert_label": "SP 인증서 (PEM, 선택)", + "allowed_attrs_label": "허용 속성 (콤마 구분, 비우면 email,username,displayName 만 전송)", + "col_name_entity": "이름 / Entity ID", + "col_acs": "ACS URL", + "col_sign": "서명", + "col_created": "생성", + "enabled": "활성", + "empty": "등록된 SAML SP 가 없습니다.", + "delete_confirm": "SP를 삭제하시겠습니까?" + }, + "poc": { + "title": "Workers 환경 PoC", + "subtitle": "킥오프 M0 사전 점검. 각 엔드포인트는 GET 요청 시 JSON 으로 결과를 반환." } } diff --git a/src/routes/(auth)/logout/+page.svelte b/src/routes/(auth)/logout/+page.svelte index d968290..3f0f2cb 100644 --- a/src/routes/(auth)/logout/+page.svelte +++ b/src/routes/(auth)/logout/+page.svelte @@ -1,11 +1,12 @@
-

로그아웃 중...

+

{t("logout.in_progress")}

diff --git a/src/routes/(auth)/mfa/+page.svelte b/src/routes/(auth)/mfa/+page.svelte index 1609fc6..907f410 100644 --- a/src/routes/(auth)/mfa/+page.svelte +++ b/src/routes/(auth)/mfa/+page.svelte @@ -1,5 +1,6 @@ @@ -49,24 +47,22 @@ const isSetupMode = $derived(!!otpauthUri);
-

2단계 인증 관리

+

{t("mfa_manage.title")}

- {#if formError}
{formError}
{/if} - {#if backupCodes}
-

백업 코드가 생성되었습니다

-

이 코드는 지금만 표시됩니다. 안전한 곳에 저장해 두세요. 코드 하나당 1회만 사용 가능합니다.

+

{t("mfa_manage.backup_codes_generated")}

+

{t("mfa_manage.backup_codes_warning")}

{#each backupCodes as code (code)} @@ -77,27 +73,26 @@ const isSetupMode = $derived(!!otpauthUri);
{/if} - {#if isSetupMode}
-

인증 앱 등록

-

Google Authenticator, Authy 등 TOTP 앱으로 QR 코드를 스캔하세요.

+

{t("mfa_manage.setup_title")}

+

{t("mfa_manage.setup_hint")}

{#if qrDataUrl} TOTP QR 코드 {:else if qrError} -

QR 코드 생성 실패

+

{t("mfa_manage.qr_error")}

{:else}
- 로딩 중... + {t("mfa_manage.qr_loading")}
{/if}
- 직접 입력 (키 보기) + {t("mfa_manage.manual_entry")}
{otpauthUri}
@@ -106,7 +101,7 @@ const isSetupMode = $derived(!!otpauthUri);
- +
- +
- - {:else if data.enrolled}
@@ -135,26 +128,26 @@ const isSetupMode = $derived(!!otpauthUri);
-

TOTP 인증기가 등록되어 있습니다.

+

{t("mfa_manage.enrolled_badge")}

{#if data.enrolledAt}

- 등록일: {new Date(data.enrolledAt).toLocaleDateString("ko-KR")} + {t("mfa_manage.enrolled_date")} + {new Date(data.enrolledAt).toLocaleDateString("ko-KR")}

{/if}
-
-

백업 코드

+

{t("mfa_manage.backup_codes_label")}

- {data.backupCodesRemaining}개 남음 + {t("mfa_manage.backup_codes_remaining", { count: data.backupCodesRemaining })} {#if data.backupCodesRemaining === 0} - — 재생성이 필요합니다 + {t("mfa_manage.backup_codes_empty")} {:else if data.backupCodesRemaining <= 3} - — 코드가 부족합니다 + {t("mfa_manage.backup_codes_low")} {/if}

@@ -165,23 +158,22 @@ const isSetupMode = $derived(!!otpauthUri); required inputmode="numeric" maxlength={6} - placeholder="TOTP 코드" + placeholder={t("mfa_manage.totp_placeholder")} class="w-28 rounded-md border border-gray-300 px-2 py-1.5 text-center font-mono text-sm tracking-widest focus:border-blue-500 focus:outline-none" />
-
- - {:else}
-

2단계 인증을 활성화하면 계정 보안이 강화됩니다. Google Authenticator, Authy 등의 인증 앱이 필요합니다.

+

{t("mfa_manage.not_enrolled_hint")}

diff --git a/src/routes/account/passkeys/+page.svelte b/src/routes/account/passkeys/+page.svelte index 22fca60..e5fa300 100644 --- a/src/routes/account/passkeys/+page.svelte +++ b/src/routes/account/passkeys/+page.svelte @@ -2,6 +2,7 @@ import { enhance } from "$app/forms"; import { invalidateAll } from "$app/navigation"; import type { ActionData, PageData } from "./$types"; +import { t } from "$lib/i18n.svelte"; const { data, form } = $props<{ data: PageData; form?: ActionData }>(); @@ -13,7 +14,6 @@ async function registerPasskey() { registerError = ""; registering = true; try { - // 1. 등록 옵션 요청 const optRes = await fetch("/api/webauthn/register/options", { method: "POST" }); if (!optRes.ok) { const msg = await optRes.text(); @@ -21,12 +21,10 @@ async function registerPasskey() { } const options = await optRes.json(); - // 2. 브라우저 패스키 등록 (클라이언트 전용) const { startRegistration } = await import("@simplewebauthn/browser"); // eslint-disable-next-line @typescript-eslint/no-explicit-any const regResponse = await startRegistration({ optionsJSON: options as any }); - // 3. 검증 요청 const verRes = await fetch("/api/webauthn/register/verify", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -59,14 +57,13 @@ const formError = $derived((form as { error?: string } | null)?.error ?? null);
-

패스키 관리

-

지문, Face ID, 보안 키 등으로 비밀번호 없이 로그인할 수 있습니다.

+

{t("passkeys.title")}

+

{t("passkeys.subtitle")}

- {#if formError}
{formError} @@ -79,7 +76,6 @@ const formError = $derived((form as { error?: string } | null)?.error ?? null);
{/if} - {#if data.passkeys.length > 0}
{#each data.passkeys as pk (pk.id)} @@ -94,12 +90,13 @@ const formError = $derived((form as { error?: string } | null)?.error ?? null);

- {pk.label ?? "패스키"} + {pk.label ?? t("account.passkeys")}

- 등록: {new Date(pk.createdAt).toLocaleDateString("ko-KR")} + {t("passkeys.registered_date")} + {new Date(pk.createdAt).toLocaleDateString("ko-KR")} {#if pk.lastUsedAt} - · 마지막 사용: {new Date(pk.lastUsedAt).toLocaleDateString("ko-KR")} + {t("passkeys.last_used")} {new Date(pk.lastUsedAt).toLocaleDateString("ko-KR")} {/if}

@@ -111,9 +108,9 @@ const formError = $derived((form as { error?: string } | null)?.error ?? null); type="submit" class="text-sm text-red-500 hover:text-red-700" onclick={(e) => { - if (!confirm("이 패스키를 삭제하시겠습니까?")) e.preventDefault(); + if (!confirm(t("passkeys.delete_confirm"))) e.preventDefault(); }}> - 삭제 + {t("common.delete")}
@@ -121,14 +118,13 @@ const formError = $derived((form as { error?: string } | null)?.error ?? null);
{:else}
-

등록된 패스키가 없습니다.

+

{t("passkeys.empty")}

{/if} -
- + - 등록 중... + {t("passkeys.registering")} {:else} - 패스키 등록 + {t("passkeys.register")} {/if}
diff --git a/src/routes/account/profile/+page.svelte b/src/routes/account/profile/+page.svelte index 91c7f07..3bbf653 100644 --- a/src/routes/account/profile/+page.svelte +++ b/src/routes/account/profile/+page.svelte @@ -1,6 +1,7 @@
-

내 프로필

+

{t("profile.title")}

{#if success} -
프로필이 저장되었습니다.
+
{t("profile.saved")}
{/if} {#if err}
@@ -34,12 +35,11 @@ const TIMEZONE_OPTIONS = [ {/if}
-
-

기본 정보

+

{t("profile.basic_info")}

- +
- +
- +
- +
- +
- +
-
-

지역 설정

+

{t("profile.locale_settings")}

- +
- + - + {#each data.kinds as k (k)} {/each}
- +
- - + +
-
- - - - - - + + + + + + {#if data.events.length === 0} - + {:else} {#each data.events as ev (ev.id)} @@ -111,7 +110,7 @@ function formatDetail(detailJson: string | null): string { @@ -127,7 +126,7 @@ function formatDetail(detailJson: string | null): string { {#if data.nextCursor !== null}
- +
{/if} diff --git a/src/routes/admin/departments/+page.svelte b/src/routes/admin/departments/+page.svelte index c8653fb..0b46504 100644 --- a/src/routes/admin/departments/+page.svelte +++ b/src/routes/admin/departments/+page.svelte @@ -1,6 +1,7 @@
-

파트 관리

+

{t("parts.title")}

{#if showCreate}
-

새 파트 추가

+

{t("parts.create_title")}

{#if createErr}

{createErr} @@ -47,7 +47,7 @@ function teamLabel(t: { name: string; departmentName: string | null }) { }} class="grid grid-cols-1 gap-3 sm:grid-cols-2">

- +
- +
- +
- +
- +
@@ -95,17 +95,17 @@ function teamLabel(t: { name: string; departmentName: string | null }) {
시각사용자이벤트결과IP상세{t("audit.col_time")}{t("audit.col_user")}{t("audit.col_event")}{t("audit.outcome")}{t("audit.col_ip")}{t("audit.col_detail")}
감사 로그가 없습니다.{t("audit.empty")}
{ev.kind} - {ev.outcome === "success" ? "성공" : "실패"} + {ev.outcome === "success" ? t("audit.success") : t("audit.failure")} {ev.ip ?? "—"}
- - - - - - + + + + + + {#if data.parts.length === 0} - + {:else} {#each data.parts as part (part.id)} @@ -124,18 +124,18 @@ function teamLabel(t: { name: string; departmentName: string | null }) {
- - + +
@@ -145,20 +145,20 @@ function teamLabel(t: { name: string; departmentName: string | null }) { {/if} diff --git a/src/routes/admin/positions/+page.svelte b/src/routes/admin/positions/+page.svelte index 00158ea..8cc33e8 100644 --- a/src/routes/admin/positions/+page.svelte +++ b/src/routes/admin/positions/+page.svelte @@ -1,6 +1,7 @@
-

팀 관리

+

{t("teams.title")}

{#if showCreate}
-

새 팀 추가

+

{t("teams.create_title")}

{#if createErr}

{createErr} @@ -43,7 +43,7 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비 }} class="grid grid-cols-1 gap-3 sm:grid-cols-2">

- + = { active: "활성", inactive: "비 class="mt-1 w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- + = { active: "활성", inactive: "비 class="mt-1 w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- +
- +
- +
@@ -91,16 +91,16 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비
파트명코드소속 팀부서상태작업{t("parts.col_name")}{t("common.code")}{t("parts.col_team")}{t("parts.col_dept")}{t("common.status")}{t("common.actions")}
등록된 파트가 없습니다.
{t("parts.empty")}
{part.teamName ?? "-"} {part.departmentName ?? "-"} - {STATUS_LABEL[part.status]} + {t(`common.status_${part.status}`)}
- +
+ if (!confirm(t("common.confirm_delete"))) e.preventDefault(); + }}>{t("common.delete")}
- - - - - + + + + + {#if data.teams.length === 0} - + {:else} {#each data.teams as team (team.id)} @@ -119,18 +119,18 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비
- - + +
@@ -139,20 +139,20 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비
{/if} diff --git a/src/routes/admin/users/+page.svelte b/src/routes/admin/users/+page.svelte index e966c00..86a6459 100644 --- a/src/routes/admin/users/+page.svelte +++ b/src/routes/admin/users/+page.svelte @@ -2,6 +2,7 @@ import { enhance } from "$app/forms"; import { resolve } from "$app/paths"; import type { ActionData, PageData } from "./$types"; +import { t } from "$lib/i18n.svelte"; const { data, form } = $props<{ data: PageData; form?: ActionData }>(); @@ -17,35 +18,31 @@ const formErr = $derived((form as { error?: string } | null)?.error ?? null); const createErr = $derived((form as { create?: boolean; error?: string } | null)?.create ? formErr : null); const resetErr = $derived((form as { resetPassword?: boolean; error?: string } | null)?.resetPassword ? formErr : null); -const STATUS_LABEL: Record = { - active: "활성", - disabled: "비활성", - locked: "잠김", -}; -const STATUS_NEXT: Record = { - active: { status: "disabled", label: "비활성화" }, - disabled: { status: "active", label: "활성화" }, - locked: { status: "active", label: "잠금 해제" }, -}; const STATUS_COLOR: Record = { active: "bg-green-100 text-green-700", disabled: "bg-gray-100 text-gray-500", locked: "bg-red-100 text-red-600", }; + +type StatusNext = { status: string; labelKey: string }; +const STATUS_NEXT: Record = { + active: { status: "disabled", labelKey: "users.action_disable" }, + disabled: { status: "active", labelKey: "users.action_enable" }, + locked: { status: "active", labelKey: "users.action_unlock" }, +};
-

사용자 관리

+

{t("users.title")}

- {#if showCreate}
-

새 사용자 추가

+

{t("users.create_title")}

{#if createErr}
@@ -64,15 +61,15 @@ const STATUS_COLOR: Record = { }} class="grid grid-cols-1 gap-3 sm:grid-cols-2">
- +
- +
- + = { class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- +
- + = { class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- +
{/if} - {#if formErr && !createErr && !resetErr}
{formErr}
{/if} -
팀명코드소속 부서상태작업{t("teams.col_name")}{t("common.code")}{t("teams.col_dept")}{t("common.status")}{t("common.actions")}
등록된 팀이 없습니다.
{t("teams.empty")}
{team.code ?? "-"} {team.departmentName ?? "-"} - {STATUS_LABEL[team.status]} + {t(`common.status_${team.status}`)}
- +
+ if (!confirm(t("common.confirm_delete"))) e.preventDefault(); + }}>{t("common.delete")}
- - - - - - + + + + + + {#if data.users.length === 0} - + {:else} {#each data.users as user (user.id)} @@ -141,7 +136,6 @@ const STATUS_COLOR: Record = { - - - - {#if resetPasswordUserId === user.id} diff --git a/src/routes/admin/users/[id]/+page.svelte b/src/routes/admin/users/[id]/+page.svelte index b0c2ba6..4037dc5 100644 --- a/src/routes/admin/users/[id]/+page.svelte +++ b/src/routes/admin/users/[id]/+page.svelte @@ -2,6 +2,7 @@ import { enhance } from "$app/forms"; import { resolve } from "$app/paths"; import type { ActionData, PageData } from "./$types"; +import { t } from "$lib/i18n.svelte"; const { data, form } = $props<{ data: PageData; form?: ActionData }>(); @@ -10,8 +11,8 @@ const updated = $derived((form as { updated?: boolean } | null)?.updated ?? fals const dateFormatter = new Intl.DateTimeFormat("ko-KR", { dateStyle: "medium" }); -function teamLabel(t: { name: string; departmentName: string | null }) { - return t.departmentName ? `${t.departmentName} / ${t.name}` : t.name; +function teamLabel(tm: { name: string; departmentName: string | null }) { + return tm.departmentName ? `${tm.departmentName} / ${tm.name}` : tm.name; } function partLabel(p: { name: string; teamName: string | null }) { @@ -32,9 +33,8 @@ const TIMEZONE_OPTIONS = [
-
- ← 목록 + {t("user_detail.back")}

{data.user.displayName ?? data.user.email}

{data.user.email}
@@ -45,16 +45,15 @@ const TIMEZONE_OPTIONS = [
{/if} {#if updated} -
저장되었습니다.
+
{t("user_detail.saved")}
{/if} -
-

프로필 정보

+

{t("user_detail.profile_section")}

- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
-
-

부서 소속

+

{t("user_detail.dept_section")}

- {#if data.deptMemberships.length > 0}
{#each data.deptMemberships as m (m.id)}
{m.departmentName} - {#if m.isPrimary}주소속{/if} + {#if m.isPrimary}{t("user_detail.primary")}{/if} {#if m.positionName}{m.positionName}{/if} {#if m.jobTitle}/ {m.jobTitle}{/if} {dateFormatter.format(m.startedAt)} ~
- +
{/each}
{:else} -

소속된 부서가 없습니다.

+

{t("user_detail.dept_empty")}

{/if} -
- +
- +
-
-

팀 소속

+

{t("user_detail.team_section")}

{#if data.teamMemberships.length > 0}
@@ -205,42 +204,45 @@ const TIMEZONE_OPTIONS = [
{m.teamName} {#if m.departmentName}({m.departmentName}){/if} - {#if m.isPrimary}주소속{/if} + {#if m.isPrimary}{t("user_detail.primary")}{/if} {#if m.jobTitle}/ {m.jobTitle}{/if} {dateFormatter.format(m.startedAt)} ~
- +
{/each} {:else} -

소속된 팀이 없습니다.

+

{t("user_detail.team_empty")}

{/if}
- +
- +
-
-

파트 소속

+

{t("user_detail.part_section")}

{#if data.partMemberships.length > 0}
@@ -249,35 +251,39 @@ const TIMEZONE_OPTIONS = [
{m.partName} {#if m.teamName}({m.teamName}){/if} - {#if m.isPrimary}주소속{/if} + {#if m.isPrimary}{t("user_detail.primary")}{/if} {#if m.jobTitle}/ {m.jobTitle}{/if} {dateFormatter.format(m.startedAt)} ~
- +
{/each} {:else} -

소속된 파트가 없습니다.

+

{t("user_detail.part_empty")}

{/if}
- +
- +
diff --git a/src/routes/poc/+page.svelte b/src/routes/poc/+page.svelte index 6542220..7c3bc0b 100644 --- a/src/routes/poc/+page.svelte +++ b/src/routes/poc/+page.svelte @@ -1,5 +1,6 @@
-

Workers 환경 PoC

-

킥오프 M0 사전 점검. 각 엔드포인트는 GET 요청 시 JSON 으로 결과를 반환.

+

{t("poc.title")}

+

{t("poc.subtitle")}

    {#each pocs as p (p.route)}
아이디 / 이메일이름역할상태생성작업{t("users.col_id_email")}{t("users.col_name")}{t("users.col_role")}{t("common.status")}{t("common.created")}{t("common.actions")}
등록된 사용자가 없습니다.{t("users.empty")}
{user.displayName ?? "-"}
@@ -149,24 +143,23 @@ const STATUS_COLOR: Record = { name="role" onchange={(e) => (e.currentTarget.closest("form") as HTMLFormElement).requestSubmit()} class="rounded border border-gray-200 bg-transparent py-0.5 text-xs text-gray-700 focus:outline-none"> - - + +
- {STATUS_LABEL[user.status]} + {t(`users.status_${user.status}`)} {#if STATUS_NEXT[user.status]}
{/if} @@ -175,11 +168,10 @@ const STATUS_COLOR: Record = {
{dateFormatter.format(user.createdAt)}
@@ -188,16 +180,15 @@ const STATUS_COLOR: Record = { type="submit" class="text-xs text-red-400 hover:text-red-600" onclick={(e) => { - if (!confirm("사용자를 삭제하시겠습니까?")) e.preventDefault(); + if (!confirm(t("users.delete_confirm"))) e.preventDefault(); }}> - 삭제 + {t("common.delete")}
@@ -220,10 +211,10 @@ const STATUS_COLOR: Record = { name="newPassword" required minlength="8" - placeholder="새 비밀번호 (8자 이상)" + placeholder={t("users.new_password_placeholder")} class="rounded-md border border-gray-300 px-3 py-1 text-sm focus:border-blue-500 focus:outline-none" /> - - + +