hehihoho3@gmail.com 07-28 f7e17ea feat: 공통코드 테이블(code_group, code) 추가 UNIX

공통코드 도입과 하드코딩 제거 설계#

작성일 2026-07-28
근거 문서: ITN-HUB 요구사항 확인요청_260727_영익 검토.pptx (28항목 중 회신분)

1. 배경#

요구사항 회신에서 "보고서 제출 완료" 13단계 신설이 확정됐다. 이 한 줄을 반영하려면 지금은
백엔드 4곳, 프론트 8곳, 테스트 6곳을 동시에 고쳐야 한다. 단계 번호와 선택지 문자열이
소스 곳곳에 리터럴로 흩어져 있기 때문이다.

회신 28항목 중 11항목이 아직 미정이다. 앞으로도 선택지 추가·이름 변경 요청이 계속 들어온다.
그때마다 같은 규모의 수정을 반복하지 않으려면 코드값을 데이터로 옮겨야 한다.

목표는 두 가지다.

  1. 코드값을 DB 공통코드로 옮기고, 소스에서 코드값 리터럴을 없앤다.
  2. 그 위에 확정된 요구사항 7건을 반영한다.

2. 현황 진단#

전수 조사 결과 같은 코드체계가 여러 곳에 중복 정의되어 있다.

코드체계정의된 곳실측
'처리완료'프론트 6 + Java 2 + SQL 513곳
공공누리 유형ReviewBoard.tsx:50(7개) / :978(5개) / ProcessBoard.tsx:29(6개)3중, 개수·순서 모두 다름
담당자 구분ContactsPage.tsx:15,33,128 + MemberDirectoryParser.java:43 + client.ts:35중
진행단계 12stages.ts:4 + StageService.java:34 + DashboardService.java:16-20 + Dashboard.tsx:31,241,244 + ProjectsPage.tsx:2405중
연락 방법ProjectsPage.tsx:32화면에만, 서버 검증 없음

구조적 사실:

  • DB에 코드테이블이 없다. 마이그레이션 V1~V12가 만드는 테이블은 8개뿐이고, 코드값 컬럼은
    전부 자유 varchar + 주석이다. CHECK 제약도 FK도 없다.
  • 백엔드에 진행단계·처리결과·공공누리유형 enum이 없다. enum은 OrgStatus, ChannelKind,
    ProvisionOutcome 3개뿐이다.
  • 코드값의 정본이 프론트 컴포넌트 파일 안에 있다. 서버는 값을 저장만 하고,
    검증은 ProcessService.java:22의 처리상태 화이트리스트 하나뿐이다.
  • 메타데이터 API가 없다(/api/meta, /api/codes 부재).
  • 화면 파일이 크다: ReviewBoard.tsx 1,279줄 / ProcessBoard.tsx 1,060줄 /
    OrgOverview.tsx 740줄 / Dashboard.tsx 697줄.

3. 저장값 정책#

기존 한글 문자열을 저장값으로 그대로 유지한다. (승인 완료)

code.code = 지금 DB에 들어 있는 값. '처리완료', '권리처리 추진' 그대로다.
진행단계만 예외로 숫자 문자열('1'~'13')이며 organization.stage는 지금처럼 smallint다.

이유: 기존 데이터를 건드리지 않고, 엑셀 업로드·다운로드 양식도 그대로 둘 수 있다.
영문 코드키 전환은 데이터 변환과 파서 전면 수정을 동반해 위험 대비 실익이 없다.
코드에서 리터럴이 사라진다는 목적은 이 방식으로도 완전히 달성된다.

4. 범위#

포함#

  • 공통코드 테이블과 시드
  • 백엔드 code 패키지, 코드 조회·관리 API
  • 프론트 codes 모듈, 화면의 하드코딩 배열 제거
  • 코드 관리 화면 (승인 완료)
  • SQL·Java 리터럴 제거, 엑셀 양식 정의 통합, 메시지 상수 분리
  • 손대는 화면 파일 분해
  • 확정 요구사항 7건 반영 (§8)

제외#

회신에서 미정이거나 결정 대기인 항목은 이번 범위에서 뺀다.

  • 미응답 10건: 1(서류제출 기준)·3(보고서작성 기준)·9(최근변경 범위)·13(초상권+개방불가)·
    14(검색 AND/OR)·18(자동 단계전환)·19(되돌림)·22(작성자 표기)·24(연락방법)·26(자료실)
  • 재질문 1건: 12(권리처리필요 자동체크)
  • 결정 대기: 20·21·27 (Mattermost 존치 여부와 사용자 계정 체계)
  • 28(보고서 양식) — 양식 수령 후 별건

단, 1·3·24는 코드테이블로 옮겨두므로 답이 오면 데이터만 바꾸면 된다.

5. 데이터 설계#

5.1 스키마#

create table code_group (
  group_id    varchar(50)  primary key,
  group_name  varchar(100) not null,
  description text,
  editable    boolean      not null default true   -- 관리 화면에서 추가·삭제를 허용할지
);

create table code (
  group_id    varchar(50)  not null references code_group(group_id),
  code        varchar(300) not null,   -- 저장값. review_minor가 varchar(300)이라 이에 맞춤
  label       varchar(200) not null,   -- 화면 표시명
  sort_order  int          not null,
  active      boolean      not null default true,
  attrs       jsonb        not null default '{}'::jsonb,
  primary key (group_id, code)
);
create index idx_code_group_sort on code (group_id, sort_order);

codelabel을 나눈 이유: 권리확인 세부항목은 저장값이
'2. 후천적 권리 전부 보유(계약서 확인 필요)'인데 화면에는
'2. 후천적 권리 전부 보유' + 회색 부연설명으로 나뉘어 보인다
(ReviewBoard.tsx:98-108). 한 컬럼으로는 기존 데이터를 유지할 수 없다.

attrs에는 코드마다 붙는 부가 속성을 넣는다. 값만 담고 동작은 담지 않는다(§5.4).

5.2 코드 그룹#

11개 그룹, 60여 개 코드. 전체 시드는 V13__common_code.sql에 둔다.

group_id내용개수대체하는 하드코딩
STAGE진행단계13stages.ts:4-17, DashboardService.java:16-20, StageService.java:34
STAGE_GROUP업무구간5stages.ts:28-34, Dashboard.tsx:34-40
REVIEW_MAJOR권리확인 대분류4ReviewBoard.tsx:18, suggestReviewResult():75-80
REVIEW_MINOR권리확인 세부5ReviewBoard.tsx:20-26, 59-63, 98-108
REVIEW_RESULT처리결과5ReviewBoard.tsx:32-42, 65-71
KOGL_TYPE공공누리 유형7ReviewBoard.tsx:50, 53, 978, ProcessBoard.tsx:29, 935, 948
PROCESS_STATUS권리처리 상태2ProcessBoard.tsx:20-24, 31, ProcessService.java:22
CONTRACT_DOC계약 서류6ProcessBoard.tsx:26, 28
CONTACT_METHOD연락 방법4ProjectsPage.tsx:32
CONTACT_CATEGORY담당자 구분5ContactsPage.tsx:15, 33, 128, MemberDirectoryParser.java:43-47
ATTACHMENT_YN / KOGL_ATTACHED첨부·부착 여부2+2ReviewBoard.tsx:939, 956

5.3 STAGE 설계 — 요구사항 [7][11][2] 반영#

code  label                    attrs
1     신청                     {"groupKey":"intake",  "progress":"applied"}
2     목록접수                 {"groupKey":"intake",  "progress":"active", "kpiFrom":"docSubmitted"}
3     예비검토                 {"groupKey":"intake",  "progress":"active"}
4     변호사 배당              {"groupKey":"assign",  "progress":"active"}
5     법률검토(권리확인)       {"groupKey":"check",   "progress":"active"}
6     RE:확인                  {"groupKey":"check",   "progress":"active"}
7     법률검토(권리확인) 완료  {"groupKey":"check",   "progress":"active"}
8     법률검토(권리처리)       {"groupKey":"process", "progress":"active"}
9     RE:처리                  {"groupKey":"process", "progress":"active"}
10    법률검토(권리처리) 완료  {"groupKey":"process", "progress":"active"}
11    법률검토 최종완료        {"groupKey":"closing", "progress":"active"}
12    보고서 작성              {"groupKey":"closing", "progress":"active", "kpiFrom":"reportWriting"}
13    보고서 제출 완료         {"groupKey":"closing", "progress":"done", "kpiFrom":"completed"}
  • progress — 요구사항 [7]. 신청은 ①만, 완료는 ⑬만, 나머지는 진행중.
    단계가 null인 기관(채널 미생성)은 progress 대상이 아니며 화면에서 '단계 미지정'으로
    따로 다룬다(승인 완료). 칸반 첫 컬럼이 이 자리다.
  • kpiFrom — "이 단계부터 해당 KPI에 집계"라는 임계값. DashboardService의 상수
    STAGE_DOC_SUBMITTED=2 / STAGE_COMPLETED=11 / STAGE_REPORT=12를 대체한다.
    요구사항 [2](보고서가 올라와야 최종완료)는 completed를 13에 두는 것으로 해결된다.
    미응답인 [1]·[3]은 현행 값을 그대로 옮겨두고, 답이 오면 데이터만 바꾼다.
  • 원문자(①②③)는 저장하지 않는다. 단계 번호에서 계산한다(U+2460 + n − 1, 20 초과 시 숫자).
    Dashboard.tsx:31CIRCLED 배열은 삭제한다.

5.4 KOGL_TYPE 설계 — 검증에서 드러난 보완점#

세 곳의 목록이 7개/5개/6개로 달랐던 것은 실수가 아니라 쓰이는 자리가 다르기 때문이다.

자리(scope)내용항목
review변호사 판정값0~4유형, 개방불가, 보류
process권리처리 판정값0~4유형, 개방불가 (보류 없음)
prior기존 부착된 유형0~4유형

sort_order 하나로는 이 차이를 담을 수 없다. attrs.scopes로 사용처를,
attrs.firstRow로 라디오 첫 줄 배치를 표현한다.

0유형    {"scopes":["review","process","prior"], "firstRow":["review","process"]}
1~4유형  {"scopes":["review","process","prior"], "firstRow":[]}
개방불가 {"scopes":["review","process"],         "firstRow":["review","process"]}
보류     {"scopes":["review"],                   "firstRow":["review"]}

이 구분 없이 단순 병합했다면 권리처리 화면에 보류가 잘못 노출된다.

5.5 색상 — DB에 CSS를 넣지 않는다#

뱃지 색이 지금 Tailwind 클래스 문자열이다(ReviewBoard.tsx:65-71).
DB에는 attrs.tone으로 의미 토큰(emerald, amber, blue, red, slate)만 저장하고,
토큰→클래스 매핑은 프론트가 갖는다. 화면 스타일 변경이 DB 수정을 부르지 않게 한다.

5.6 값과 동작의 경계#

applyResultDerivations(ReviewBoard.tsx:812-834)를 실측한 결과, 처리결과 선택 시 동작은
단순 대입이 아니었다.

처리결과동작
권리처리 추진공공누리 보류 대입, 권리처리필요 켬
개방불가공공누리 개방불가 대입, 권리처리필요 끔
권리처리 추진 미희망권리처리필요 끔, 기존 부착 유형을 복사, 비고에 안내문구 덧붙임

마지막 행의 "복사"와 "덧붙임"까지 데이터로 옮기면 attrs 안에 규칙 언어를 새로 만드는
꼴이 된다. 값은 데이터로, 동작은 코드로 나눈다.

신유형 개방          {"tone":"emerald","openable":"Y"}
계약서 등 재확인     {"tone":"amber"}
권리처리 추진        {"tone":"blue",  "needsProcessing":true,  "koglSet":"보류"}
개방불가             {"tone":"red",   "needsProcessing":false, "koglSet":"개방불가"}
권리처리 추진 미희망 {"tone":"slate", "needsProcessing":false, "koglCopiesPrior":true}

koglCopiesPrior가 참일 때 무엇을 복사하고 어떤 문구를 붙일지는 코드가 정한다.
문구 자체는 메시지 상수 모듈로 옮긴다. 코드에서 리터럴이 사라진다는 목적은 유지된다.

needsProcessing을 데이터로 둔 이유는 이것이 요구사항 [12]의 대상이기 때문이다.
자동 처리를 끄기로 결정되면 코드 수정 없이 속성만 제거하면 된다.

5.7 기존 데이터 정합성#

운영 DB에 시드와 일치하지 않는 값이 남아 있을 수 있다(정책 변경 전 값, 공백 차이, 오타).
Phase 1에 정합성 점검을 넣는다.

select 'review_result' as col, review_result as value, count(*)
  from review_item
 where review_result is not null and review_result <> ''
   and review_result not in (select code from code where group_id = 'REVIEW_RESULT')
 group by review_result;

review_major, review_minor, judged_kogl_type, process_status, contact_log.method
같은 점검을 돌린다. 발견된 고아 값은 삭제하지 않고 active = false로 코드테이블에
추가한다. 기존 화면에서 값이 사라지는 사고를 막는다.

6. 애플리케이션 설계#

6.1 백엔드 — kr.itn.itnhub.code#

클래스역할
Code, CodeGrouprecord. attrsMap<String,Object>
CodeMapper + CodeMapper.xml조회·저장
CodeService기동 시 전체 로드 후 메모리 캐시. 저장 시 캐시 갱신
CodeController조회·관리 API
Codes그룹 ID 상수와 로직이 참조하는 코드 상수
CodeValidator저장 요청 값이 해당 그룹의 활성 코드인지 검증

코드값은 자주 바뀌지 않으므로 캐시는 ConcurrentHashMap 한 겹으로 충분하다.
분산 캐시는 넣지 않는다(단일 인스턴스 운영).

API:

GET    /api/meta/codes                  전체 코드 (프론트 부팅 시 1회)
GET    /api/admin/codes/groups          관리 화면용 그룹 목록
GET    /api/admin/codes/{groupId}       그룹별 코드 목록 + 사용 건수
POST   /api/admin/codes/{groupId}       코드 추가
PUT    /api/admin/codes/{groupId}/{code} 라벨·순서·사용여부·attrs 수정
DELETE /api/admin/codes/{groupId}/{code} 코드 삭제 (사용 중이면 거부)

6.2 프론트 — frontend/src/codes/#

codes/
  types.ts       Code, CodeMap 타입
  CodeProvider.tsx  로그인 후 1회 조회, Context 제공
  useCodes.ts    useCodes('REVIEW_RESULT'), useCode(group, code)
  stage.ts       단계 전용 헬퍼 (label, symbol, groupKey, progress, kpiFrom)
  tone.ts        tone 토큰 → Tailwind 클래스 매핑

기존 frontend/src/stages.ts는 삭제하고 codes/stage.ts로 대체한다.

로딩 시점은 인증 성공 직후다. 로그인 화면에서는 코드가 필요 없다.
조회 실패 시 화면을 그리지 않고 오류를 표시한다(빈 선택지로 저장되는 사고 방지).

각 화면 상단의 옵션 배열은 전부 삭제하고 useCodes로 대체한다.

6.3 코드 관리 화면#

좌측 그룹 목록, 우측 코드 목록. 사이드바에 '공통코드' 메뉴를 추가한다.

  • 편집 가능: 표시명, 정렬순서, 사용여부, 코드 추가·삭제
  • attrs는 접이식 JSON 편집기로 노출하고 경고 문구를 붙인다.
    그룹별 전용 폼은 만들지 않는다. 실제 편집 수요는 표시명과 순서에 몰려 있고,
    전용 폼은 그룹이 늘 때마다 화면을 늘려야 한다.
  • 삭제 안전장치: 해당 코드를 쓰는 데이터 건수를 먼저 조회하고, 1건 이상이면 삭제를 막고
    사용여부 끄기를 안내한다.
  • code_group.editable = false인 그룹(STAGE 등 로직 결합이 강한 그룹)은 추가·삭제를 막고
    표시명·순서만 허용한다. 단계 추가는 마이그레이션으로 한다.
  • 권한: 지금은 인증만으로 충분하다(관리자 1계정). 사용자 계정 도입 시 관리자 전용으로 제한한다.

6.4 코드테이블 밖에서 처리하는 것#

SQL 안의 '처리완료' 5곳 — 쿼리라서 코드테이블로 뺄 수 없다. MyBatis 파라미터로 주입한다.
대상: ProcessItemMapper.xml:42, 45, 148, 176, 194, DashboardMapper.xml:78.
매퍼 인터페이스 시그니처에 doneStatus를 추가하고 서비스가 Codes에서 읽어 넘긴다.

엑셀 양식 정의 — 코드값이 아니라 파일 포맷이다. 코드테이블에 넣지 않는다.
다만 지금 업로드 쪽(ReviewParser.COL_* 23개, ProcessParser.COL_* 25개)과
다운로드 쪽(ReviewService.HEADERS 25개, ProcessService.HEADERS 27개)이 서로 독립 정의라
한쪽만 고치면 양식이 어긋나는 결함이 있다. 열 순서·헤더명·필드를 한 곳에서 선언하는
스키마 모듈로 합치고, 업로드와 다운로드가 같은 정의를 참조하게 한다.

주의: ProcessParser.java:66-68에 "헤더는 T열인데 데이터는 U열" 예외 처리가 있다.
통합 시 이 예외를 놓치면 권리처리 업로드가 조용히 어긋난다.

안내 문구NOTE_NO_PROCESSING 등은 코드값이 아니라 메시지다. 메시지 상수 모듈로 분리한다.

Mattermost 채널 종류ChannelKind enum과 설정 프로퍼티로 이미 관리된다. 그대로 둔다.

6.5 화면 파일 분해#

손대는 파일만 분해한다. 무관한 파일은 건드리지 않는다.

현재분해
ReviewBoard.tsx 1,279ReviewBoard / ReviewFilters / ReviewTable / ReviewDetail / ReviewBulkForm
ProcessBoard.tsx 1,060ProcessBoard / ProcessFilters / ProcessTable / ProcessDetail / ProcessBulkForm
Dashboard.tsx 697Dashboard / KpiTiles / KanbanBoard / RecentOrgTable
OrgOverview.tsx 740OrgOverview / OrgStageStrip / OrgTabs / ReTab(신규)

7. 실행 순서#

Phase 14는 기존 동작이 바뀌지 않는다. Phase 5는 화면을 새로 추가할 뿐 기존 동작을
건드리지 않는다. 기존 테스트 40개(백엔드 22 + 프론트 18)가 그대로 통과해야 한다는 것이
Phase 1
5 각 단계의 완료 조건이며, 이것이 이번 작업의 안전망이다.
실제 동작이 바뀌는 것은 Phase 6뿐이다.

Phase내용동작 변화
1코드테이블 + 시드 + code 패키지 + /api/meta/codes + 정합성 점검없음
2프론트 codes 모듈, 화면이 코드테이블에서 옵션을 읽도록 전환, 하드코딩 배열 삭제없음
3Java·SQL 리터럴 제거, 엑셀 양식 정의 통합, 메시지 상수 분리없음
4화면 파일 분해없음
5코드 관리 화면화면 추가
6요구사항 반영 (§8)있음

8. 요구사항 반영 (Phase 6)#

항목내용작업
[7][11]13단계 "보고서 제출 완료" 신설, 진행상태 신청/진행중/완료시드에 13 추가, StageService 상한을 코드 개수로, 칸반 컬럼 = 단계미지정 + 13
[2]완료 = 보고서 업로드 기준kpiFrom:"completed"를 13에 부여
[4][5]RE 메모 기능 신설re_memo 테이블, re 패키지, 기관관리 RE 탭, 대시보드 미해결 RE 집계
[15]일괄등록에 비고 추가BulkJudgmentRequestlawyerNote, BulkProcessingRequestreviewNote
[8]자료접수 = 권리확인·권리처리 통합Dashboard의 판정에 processTotal 포함
[6]업무구간 필터 삭제, 필터 순서 재배치필터 UI만 제거. STAGE_GROUP은 칸반 색·카드 모양에 계속 쓰이므로 유지
[25]보고서 수정PUT /api/orgs/{id}/reports/{reportId} 신설
[16]표기 누락 점검현행 2줄 표기 유지, 열 누락 여부만 확인
[17][21][10]현행 유지작업 없음
[23]기관 100개코드 변경 없음. 명부 엑셀 수령 후 업로드

RE 메모 설계#

회신 예시: ooo 계약서 및 제안요청서 송부 요청, 장영익, 2026.07.08

create table re_memo (
  id          bigserial primary key,
  org_id      bigint not null references organization(id),
  content     text not null,
  author      varchar(100) not null,
  memo_date   date not null,
  resolved    boolean not null default false,
  resolved_at timestamptz,
  created_at  timestamptz not null default now(),
  updated_at  timestamptz not null default now()
);

메모 1건 = RE 1건. 기관관리 RE 탭에서 목록·등록·수정·삭제한다.
대시보드 '미해결 RE'는 resolved = false 건수 합계다.
DashboardMapper.xml:640 as re_count를 서브쿼리로 교체한다
(해당 파일 44-45행 주석이 이미 이 방식을 예고하고 있다).

확인받지 못한 가정: resolved 플래그는 회신에 없던 항목이다. 대시보드 '미해결 RE'
숫자를 세려면 해결 여부를 구분할 수단이 필요해 완료 체크박스로 설계했다.
이 가정이 틀리면 RE 탭과 KPI 집계를 함께 손봐야 한다. 회신 요청 상태다.

9. 테스트#

기존 테스트는 리팩터링의 회귀 안전망이다. Phase 1~5 동안 계속 통과해야 한다.

신규로 필요한 것:

  • CodeServiceTest — 캐시 적재·갱신, scope 필터, 활성 코드만 노출
  • CodeControllerTest — 조회·수정·삭제, 사용 중 코드 삭제 거부
  • DashboardServiceTest현재 없다. kpiFrom 기반 집계로 바꾸기 전에 먼저 만들어
    현행 동작을 고정한다
  • ReviewParserTest / ProcessParserTest현재 없다. 엑셀 양식 정의를 통합하기 전에
    업로드→다운로드 왕복 테스트를 만들어 양식 호환을 고정한다. ProcessParser의 T/U열 예외를
    반드시 포함한다
  • ReMemoControllerTest — RE 메모 CRUD
  • 프론트: codes 모듈 테스트, 분해된 컴포넌트별 테스트

Phase 6에서 갱신이 필요한 기존 테스트:

  • StageControllerTest:90,92 — "13은 400" 단언이 뒤집힌다
  • DashboardControllerTest:57,96,98 — 11/12 기준 단언
  • OrgOverview.test.tsx:254,466 — "업무단계 12개"

10. 리스크#

리스크대응
운영 DB에 시드와 다른 값이 남아 있음Phase 1 정합성 점검, 고아 값은 active=false로 보존
엑셀 양식 통합 시 기존 파일 호환 깨짐왕복 테스트를 먼저 작성, T/U열 예외 포함
STAGE_GROUP을 필터와 함께 지워 칸반이 깨짐필터 UI만 제거, 색·카드 모양 용도는 유지 (§8)
코드 조회 실패 시 빈 선택지로 저장조회 실패 시 화면을 그리지 않고 오류 표시
Phase 4 분해 중 회귀각 Phase 완료 조건 = 기존 테스트 전량 통과

11. 미결 항목#

  • RE 메모의 resolved 플래그 (§8) — 회신 대기
  • 요구사항 미응답 10건, 재질문 1건, 결정 대기 3건 — §4 제외 목록
  • 기관 100개 명부 엑셀 — 수령 대기
  • 주간·월간 보고 양식 — 수령 대기