신청기관 담당자 1종만 저장하던 기관 테이블에 문정원 담당자(부서/담당자/연락처/이메일)와
담당 변호사(성명/연락처/이메일/배정일) 정보를 추가해, 채널관리 화면에서 세 담당자를
한 번에 저장하고 다른 화면(기관관리·업무메모)에서도 쓸 수 있게 한다.
- V3 마이그레이션으로 organization에 컬럼 8개 추가 (upsertBySeed는 그대로 둬서
엑셀 재시드가 새 컬럼을 건드리지 않는다)
- ContactRequest/OrgResponse/updateContact에 8개 필드를 선택값으로 반영
- 컨트롤러 테스트 2건(신규 필드 왕복, 신규 필드 없는 기존 페이로드) +
매퍼 테스트 2건(영속화 왕복, 재시드 시 보존) 추가
placeholder였던 업무메모 탭을 실제 화면으로 교체한다. 담당자를 기관
담당자 중에서 고르거나 직접 입력할 수 있고, 등록한 메모는 최신순으로
보여주며 확인 후 삭제할 수 있다.
- client.ts: WorkMemo 타입 및 getMemos/createMemo/deleteMemo 추가
- WorkMemos.tsx: 담당자 선택(기관 담당자/직접 입력) + 메모 등록/목록/삭제
- OrgOverview.tsx: 업무메모 탭에 WorkMemos 연결
- WorkMemos.test.tsx, OrgOverview.test.tsx: getMemos 목업 포함 테스트 추가
Co-Authored-By: Claude Opus 4.8 (1M context)
기관관리 상세의 업무메모 탭이 쓸 CRUD 엔드포인트를 추가한다. 통화 등
담당자와의 연락 내용을 기관별로 남기는 단순 기능이라 Mattermost 연동
없이 순수 DB로만 처리한다.
- V2__work_memo.sql: work_memo 테이블, org_id+created_at 인덱스
- WorkMemoMapper/xml: 최신순 조회, 등록, (orgId, id) 동시 조건 삭제
- WorkMemoService/Controller: GET/POST /api/orgs/{id}/memos,
DELETE /api/orgs/{id}/memos/{memoId}
- GlobalExceptionHandler: contactName/body 검증 메시지 한글 라벨 추가
- WorkMemoControllerTest: 등록/조회/최신순/검증/404/삭제/인증 검증
Co-Authored-By: Claude Opus 4.8 (1M context)
- ChannelPosts: 문정원/법률검토 채널 선택 칩 + 3초 폴링으로 최근 게시글을 보여주고,
시스템 메시지·첨부파일 다운로드 칩·자동 스크롤을 처리한다
- ChannelFiles: 같은 채널 선택으로 자료실 파일 전체를 표로 보여주고 새로고침을 지원한다
- client.ts에 getPosts/getFiles/fileDownloadUrl 추가, OrgOverview는 게시물/자료 탭에서
자리표시 대신 위 컴포넌트를 렌더링(org.id로 key를 잡아 기관 전환 시 폴링을 재시작)
- MattermostClient에 게시글 조회(getRecentPosts), 채널 파일 수집(collectChannelFiles),
파일 다운로드/정보 조회(downloadFile, fileInfo)를 추가하고 사용자 이름을 캐시해서 반복
조회를 줄인다
- ChannelFeedService/Controller로 기관별 문정원/법률검토 채널의 게시글·자료 목록과
파일 다운로드 프록시(GET /api/orgs/{id}/posts|files, GET /api/files/{fileId})를 제공
- 채널 미생성/잘못된 channel 파라미터는 ChannelNotReadyException(409)으로 구분한다
- 시스템 개요, 필수 환경변수 8개 표, 실운영 Mattermost 경고 박스(system-admin PAT
필요, 테스트 기관으로 스모크 권장), 빌드/실행/최초 사용 순서, hikari.schema
설정이 왜 필요한지, 프론트엔드 개발 워크플로를 정리.
- .env.example에 8개 변수를 플레이스홀더 값으로 정리하고 실값 커밋 금지 주석 추가.
- .gitignore에 .env 추가.
Co-Authored-By: Claude Opus 4.8 (1M context)
- OrgDetail의 초기화 effect를 org 객체 identity 대신 org.id에 키잉해, 채널 생성
성공 후 App.reload()가 만드는 새 객체가 방금 받은 provision 결과/에러/입력 중이던
담당자 정보를 지워버리지 않게 함(다른 기관으로 전환할 때는 그대로 초기화).
- 양쪽 채널이 모두 실패했을 때 "문정원 채널은 살아있다"는 잘못된 안내를 보여주던
문제를 3분기(둘 다 실패/법률검토만 실패/문정원만 실패)로 재구성해 해결.
- 정보 저장/채널 생성/시드 업로드에 busy 상태를 추가해 처리 중 중복 클릭으로 두
요청이 동시에 나가는 것을 방지. ConfirmDialog도 busy 중엔 두 버튼을 비활성화.
- api/client에 상태코드를 담는 ApiError를 도입해 401만 로그아웃으로 취급하고,
그 외 오류는 재시도 버튼이 있는 오류 화면으로 안내(App.tsx).
- 필수 담당자 정보(부서명/담당자명/연락처/이메일)가 비어 있으면 정보 저장 버튼을
비활성화(직급/직함은 선택 유지).
- 로그인/기관검색 입력에 aria-label과 autoComplete 추가, StatusBadge READY 케이스
테스트 추가.
Co-Authored-By: Claude Opus 4.8 (1M context)
- findChannelIdByDisplayName이 /users/me/teams/{id}/channels(토큰 사용자가 가입한
채널만) 대신 /teams/{id}/channels를 페이지네이션(page/per_page=200, 상한 100페이지)
하며 표시명을 찾도록 변경. 레거시 채널에 토큰 사용자가 가입하지 않은 경우 생기던
중복 채널 생성 위험을 제거한다(system-admin PAT 필요, README에 명시).
- 존재하지 않는 기관 id에 대한 404를 폭넓은 IllegalArgumentException 대신 전용
OrgNotFoundException으로 좁혀서 매핑.
- 검증 실패(@Valid) 응답을 원시 필드명이 담긴 JSON 대신 한글 라벨 메시지로 변환.
- SeedController 업로드 스트림 읽기 실패를 SeedParseException(400)으로 통일.
Co-Authored-By: Claude Opus 4.8 (1M context)
- GlobalExceptionHandler(@RestControllerAdvice) 추가: IllegalArgumentException은
404, SeedParseException은 400으로 매핑하고 예외 메시지를 ApiError로 그대로
돌려준다. Exception 전체를 잡는 catch-all은 두지 않아 예상 밖의 진짜 버그는
여전히 500으로 드러나게 한다.
- SeedController 업로드에 확장자(.xlsx/.xlsm) 검사를 추가해 임의의 바이트가
Apache POI로 그대로 전달되기 전에 SeedParseException으로 걸러낸다.
- POST /api/orgs/{id}/channels의 실제 혼합 성공/실패 결과가 200으로 내려가는
경로를 컨트롤러 경계에서 검증하는 테스트를 추가하고, 담당자 정보 누락
테스트의 law 단정을 보강했다.
- CookieCsrfTokenRepository.saveToken()이 한 번도 호출되지 않아 브라우저가
XSRF-TOKEN 쿠키를 영영 못 받던 문제를 Spring Security 레퍼런스가 SPA용으로
제시하는 방식대로 고쳤다. BasicAuthenticationFilter 뒤에 지연 토큰을 강제
resolve하는 필터를 추가하고, 기본 XorCsrfTokenRequestAttributeHandler가
BREACH 마스킹 때문에 쿠키 원문과 헤더 값을 불일치시키던 문제를
SpaCsrfTokenRequestHandler로 해결했다(헤더 존재 시 마스킹 해제 없이 신뢰,
파라미터 기반 검증은 기존 Xor 위임 유지).
- "로그인 엔드포인트는 인증없이 접근할 수 있다" 테스트가 permitAll() 삭제와
무관하게 항상 같은 401을 반환해 아무것도 증명하지 못하던 문제를, 인증 실패와
인가 차단이 세션 생성 여부에서 실제로 다르다는 점(ExceptionTranslationFilter의
RequestCache)으로 구분하도록 재작성했다.
- AdminProperties record의 toString()이 비밀번호를 그대로 노출하지 않도록
마스킹했다.
- formLogin에 커스텀 loginPage를 지정해 DefaultLoginPageGeneratingFilter가
"/login"을 가로채지 않도록(3단계 SPA 클라이언트 라우트 확보) 했다.
- CSRF 쿠키 왕복 회귀 테스트, 토큰 누락 시 응답 코드, 로그인 페이지 미노출을
검증하는 테스트 3건을 추가했다. with(csrf())가 캐시된 컨텍스트의 CsrfFilter
빈을 영구히 세션 기반으로 바꿔치기하는 부작용이 있어 @TestMethodOrder로 실행
순서를 고정했다.
Co-Authored-By: Claude Opus 4.8 (1M context)
MattermostException만 catch하던 ensure()를 RuntimeException으로 넓혀,
채널 생성 직후 mapper 저장이 DataAccessException 계열로 실패해도
반대쪽 채널 시도를 막지 않고 FAILED로 강등되도록 수정. applyChannelId는
exhaustive switch 식으로 바꿔 컬럼 오기입 방지 주장을 실제로 컴파일
타임에 보장하고, 첫 채널 실패/양쪽 실패/DB 저장 실패 회귀 테스트를
추가했다.
Co-Authored-By: Claude Opus 4.8 (1M context)