+ * @Auditable(action = "CONFIRM_PARENT", resourceType = "GEAR_GROUP")
+ * public void confirmParent(String groupKey, ...) { ... }
+ *
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Auditable {
+ /** 액션 코드 (예: CONFIRM_PARENT, REJECT_PARENT, USER_CREATE, ROLE_GRANT, PERM_UPDATE) */
+ String action();
+
+ /** 리소스 타입 (예: VESSEL, GROUP, USER, ROLE, SYSTEM) */
+ String resourceType() default "SYSTEM";
+}
diff --git a/backend/src/main/java/gc/mda/kcg/auth/AccountSeeder.java b/backend/src/main/java/gc/mda/kcg/auth/AccountSeeder.java
new file mode 100644
index 0000000..f57abfb
--- /dev/null
+++ b/backend/src/main/java/gc/mda/kcg/auth/AccountSeeder.java
@@ -0,0 +1,63 @@
+package gc.mda.kcg.auth;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import java.util.Map;
+
+/**
+ * 데모 계정 5종의 BCrypt 해시 시드/갱신 (시동 시 1회).
+ * V006이 PLACEHOLDER로 계정을 만들었고, 이 Runner가 실제 해시를 채워넣음.
+ *
+ * 데모 계정 비밀번호 (LoginPage의 DEMO_ACCOUNTS와 동일):
+ * admin / admin1234!
+ * operator / oper12345!
+ * analyst / anal12345!
+ * field / field1234!
+ * viewer / view12345!
+ *
+ * 기존 해시가 PLACEHOLDER가 아니면 갱신하지 않음 (운영 중 비밀번호 변경 보존).
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class AccountSeeder {
+
+ private static final String PLACEHOLDER = "PLACEHOLDER_TO_BE_SEEDED";
+
+ private static final Map
+ * @RequirePermission(resource = "parent-inference-workflow:parent-review", operation = "UPDATE")
+ * @PostMapping("/groups/{key}/parent-inference/{sub}/review")
+ * public ResponseEntity> review(...) { ... }
+ *
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RequirePermission {
+ /** 리소스 코드 (예: "detection:gear-detection") */
+ String resource();
+
+ /** 오퍼레이션 (READ/CREATE/UPDATE/DELETE/EXPORT). 기본 READ */
+ String operation() default "READ";
+}
diff --git a/backend/src/main/resources/db/migration/V001__auth_init.sql b/backend/src/main/resources/db/migration/V001__auth_init.sql
index 4f6da55..9b785e0 100644
--- a/backend/src/main/resources/db/migration/V001__auth_init.sql
+++ b/backend/src/main/resources/db/migration/V001__auth_init.sql
@@ -15,7 +15,7 @@ CREATE TABLE kcg.auth_org (
org_tp_cd VARCHAR(20), -- HQ, REGIONAL, STATION, AGENCY
upper_org_sn BIGINT REFERENCES kcg.auth_org(org_sn),
sort_ord INT DEFAULT 0,
- use_yn CHAR(1) NOT NULL DEFAULT 'Y',
+ use_yn VARCHAR(1) NOT NULL DEFAULT 'Y',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
@@ -60,8 +60,8 @@ CREATE TABLE kcg.auth_role (
role_cd VARCHAR(50) UNIQUE NOT NULL, -- ADMIN, OPERATOR, ANALYST, VIEWER, FIELD
role_nm VARCHAR(100) NOT NULL,
role_dc TEXT,
- dflt_yn CHAR(1) NOT NULL DEFAULT 'N', -- 신규 사용자 자동 배정 여부
- builtin_yn CHAR(1) NOT NULL DEFAULT 'N', -- 내장 역할 (삭제 불가)
+ dflt_yn VARCHAR(1) NOT NULL DEFAULT 'N', -- 신규 사용자 자동 배정 여부
+ builtin_yn VARCHAR(1) NOT NULL DEFAULT 'N', -- 내장 역할 (삭제 불가)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
diff --git a/backend/src/main/resources/db/migration/V002__perm_tree.sql b/backend/src/main/resources/db/migration/V002__perm_tree.sql
index 8c425e6..3674dbe 100644
--- a/backend/src/main/resources/db/migration/V002__perm_tree.sql
+++ b/backend/src/main/resources/db/migration/V002__perm_tree.sql
@@ -13,7 +13,7 @@ CREATE TABLE kcg.auth_perm_tree (
icon VARCHAR(50),
rsrc_level INT NOT NULL DEFAULT 0, -- 0=tab(권한그룹), 1=subtab/패널, 2+=중첩
sort_ord INT NOT NULL DEFAULT 0,
- use_yn CHAR(1) NOT NULL DEFAULT 'Y',
+ use_yn VARCHAR(1) NOT NULL DEFAULT 'Y',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
@@ -32,7 +32,7 @@ CREATE TABLE kcg.auth_perm (
role_sn BIGINT NOT NULL REFERENCES kcg.auth_role(role_sn) ON DELETE CASCADE,
rsrc_cd VARCHAR(100) NOT NULL REFERENCES kcg.auth_perm_tree(rsrc_cd) ON DELETE CASCADE,
oper_cd VARCHAR(20) NOT NULL, -- READ, CREATE, UPDATE, DELETE, EXPORT, MANAGE
- grant_yn CHAR(1) NOT NULL, -- Y(허용), N(명시적 거부)
+ grant_yn VARCHAR(1) NOT NULL, -- Y(허용), N(명시적 거부)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by UUID,
UNIQUE(role_sn, rsrc_cd, oper_cd)
diff --git a/backend/src/main/resources/db/migration/V006__demo_accounts.sql b/backend/src/main/resources/db/migration/V006__demo_accounts.sql
new file mode 100644
index 0000000..56530a1
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V006__demo_accounts.sql
@@ -0,0 +1,28 @@
+-- ============================================================================
+-- V006: 데모 계정 5종 생성 (해시는 AccountSeeder가 시동 시 갱신)
+-- ============================================================================
+-- 향후 운영 배포 시에도 데모 계정은 유지됨 (운영자가 비활성화 가능)
+-- 비밀번호는 AccountSeeder.java가 BCrypt로 시동 시 한 번만 시드/갱신
+-- ----------------------------------------------------------------------------
+
+-- 기존 admin placeholder 제거 (V003에서 만든 행)
+DELETE FROM kcg.auth_user_role WHERE user_id IN (SELECT user_id FROM kcg.auth_user WHERE user_acnt = 'admin');
+DELETE FROM kcg.auth_user WHERE user_acnt = 'admin';
+
+-- 데모 계정 5종 생성 (pswd_hash는 placeholder, AccountSeeder가 BCrypt로 갱신)
+INSERT INTO kcg.auth_user (user_acnt, user_nm, rnkp_nm, email, user_stts_cd, auth_provider, pswd_hash) VALUES
+ ('admin', '김영수', '사무관', 'admin@kcg.go.kr', 'ACTIVE', 'PASSWORD', 'PLACEHOLDER_TO_BE_SEEDED'),
+ ('operator', '이상호', '경위', 'operator@kcg.go.kr', 'ACTIVE', 'PASSWORD', 'PLACEHOLDER_TO_BE_SEEDED'),
+ ('analyst', '정해진', '주무관', 'analyst@kcg.go.kr', 'ACTIVE', 'PASSWORD', 'PLACEHOLDER_TO_BE_SEEDED'),
+ ('field', '박민수', '경사', 'field@kcg.go.kr', 'ACTIVE', 'PASSWORD', 'PLACEHOLDER_TO_BE_SEEDED'),
+ ('viewer', '최원석', '6급', 'viewer@kcg.go.kr', 'ACTIVE', 'PASSWORD', 'PLACEHOLDER_TO_BE_SEEDED');
+
+-- 역할 매핑 (계정 → 역할)
+INSERT INTO kcg.auth_user_role (user_id, role_sn)
+SELECT u.user_id, r.role_sn
+FROM kcg.auth_user u, kcg.auth_role r
+WHERE (u.user_acnt = 'admin' AND r.role_cd = 'ADMIN')
+ OR (u.user_acnt = 'operator' AND r.role_cd = 'OPERATOR')
+ OR (u.user_acnt = 'analyst' AND r.role_cd = 'ANALYST')
+ OR (u.user_acnt = 'field' AND r.role_cd = 'FIELD')
+ OR (u.user_acnt = 'viewer' AND r.role_cd = 'VIEWER');
diff --git a/frontend/src/app/auth/AuthContext.tsx b/frontend/src/app/auth/AuthContext.tsx
index d0d02ab..4de13af 100644
--- a/frontend/src/app/auth/AuthContext.tsx
+++ b/frontend/src/app/auth/AuthContext.tsx
@@ -1,11 +1,13 @@
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
+import { fetchMe, loginApi, logoutApi, LoginError, type BackendUser } from '@/services/authApi';
/*
* SFR-01: 시스템 로그인 및 권한 관리
- * - 역할 기반 권한 관리(RBAC)
- * - 세션 타임아웃(30분 미사용 시 자동 로그아웃)
- * - 동시 접속 1계정 1세션
- * - 감사 로그 기록
+ * - 백엔드 JWT 쿠키 기반 인증
+ * - 트리 기반 RBAC (백엔드의 auth_perm_tree + auth_perm)
+ * - 다중 역할 + 부모 fallback (예: detection:gear-detection 미존재 시 detection 검사)
+ * - 세션 타임아웃: 30분 미사용 시 자동 로그아웃
+ * - 로그인 이력 + 감사로그는 백엔드 DB(kcgaidb)에 기록
*/
// ─── RBAC 역할 정의 ─────────────────────
@@ -13,95 +15,125 @@ export type UserRole = 'ADMIN' | 'OPERATOR' | 'ANALYST' | 'FIELD' | 'VIEWER';
export interface AuthUser {
id: string;
+ /** 로그인 ID */
+ account: string;
name: string;
rank: string;
org: string;
+ /** 다중 역할 (백엔드는 배열 반환) */
+ roles: UserRole[];
+ /** 1차 역할 (기존 코드 호환) */
role: UserRole;
+ /** 권한 트리: rsrcCd → operations[] */
+ permissions: Record{t('gpki.title')}
-{t('gpki.desc')}
-- {t('gpki.internalOnly')} -
+{t('gpki.title')}
+향후 도입 예정 (Phase 9)
{t('sso.title')}
-{t('sso.desc')}
-- {t('sso.sessionNote')} -
+{t('sso.title')}
+향후 도입 예정 (Phase 9)