장애 보고서 · LAWLIN-2026-0825

업무분야 ‘ESG·컴플라이언스’ 실종 — 추적·복구·재발방지

표기 안내. 서버·DB 주소와 비밀번호, 외부 노출이 곤란한 IP는 *** 로 가렸습니다. 업무 맥락상 필요한 구성원 실명은 그대로 두었습니다. 이 페이지는 검색엔진에 노출되지 않으나(noindex), 주소를 아는 사람은 열람할 수 있습니다.
차례
  1. 요약본
  2. 핵심 포인트
  3. 전체 대화 원문

1. 요약본

한 줄 결론

코드가 지운 것이 아니라 사람이 어드민 삭제 버튼으로 두 번 지웠고, 8/19에 한 번 복구했으나 8/21에 다시 지워진 것이다. 감사 기록이 없어 그동안 추적이 불가능했다. 복구를 마쳤고, 재발방지 조치 네 가지를 운영에 배포했다.

확정 타임라인 (KST)

시각사건근거
8/13 11:54:44deleteExpertise 호출 (이번 건 이전)nginx 접근 로그
8/19 15:19~15:35ESG 조직 개편 — 카테고리 ec_idx=21 을 ‘탄소기후그룹’ 으로 개명백업 테이블 created_at
8/19 15:38:461차 삭제 — 업무분야 e_idx=230 삭제nginx 접근 로그
8/19 17:58:46복원 스크립트 운영 실행 — 행 + 구성원 6명 연결 복구송유빈 행 created_at, 백업 테이블 3개 존재
8/19 18:01커밋 159a557 (문서에는 “운영 미적용” 으로 잘못 기재)git
8/21 16:53:142차 삭제 — 다시 삭제됨. 담당자가 “또 사라졌다” 고 제보한 그것nginx 접근 로그
8/25 16:1x운영 복원 완료본 작업
8/25 16:3x재발방지 조치 운영 배포 완료본 작업

2차 삭제 주체

2026-08-21 16:53:14 KST   POST /lawlin/admin/deleteExpertise
IP        220.117.***.**        (오늘 감사 로그의 actor=admin 과 동일한 사무실 IP)
브라우저  Windows 10 · Edge 151 (Chromium)
경유      https://admin.law-lin.com/

세션 흐름은 로그인 62초 만에 조회 → 수정 → 삭제였다. 카테고리 정리 작업 중 연달아 처리한 흐름이고 삭제 후 별다른 조치 없이 목록만 새로고침했다. 의도적 개편보다 실수 쪽에 가깝다.

복구 결과

검증결과
업무분야 e_idx=230부활 — 4개 언어 전부, showset='Y'
선택 목록(selectExpertiseCode) 노출
연결된 구성원6명 (박경희·조선희·김종식·강민구·이석·송유빈)
송유빈 화면의 230ESG·컴플라이언스 로 정상 표시
이름 안 붙는 고아0건
ref_expertise 참조3건 복구 (54, 207, 248)

담당자의 요청 두 건(“복구해달라” + “송유빈 연구원 업무분야에도 추가해달라”)이 한 번에 해결됐다. 송유빈의 연결은 이미 고아 상태로 살아 있었고 본체만 없었기 때문에, 본체 복구만으로 이름을 되찾았다.

재발방지 — 운영 배포 완료

조치내용검증
어드민 변경 감사 로그 /lawlin/admin/** 인터셉터. 조회 제외, 변경만 actor/ip/action/식별 파라미터/status 기록 운영 로그에 실제 기록 확인
삭제 시 매핑 cascade 정리 @Transactional + 정리 5단계. ref_expertise 는 해당 번호만 제거 테스트 업무분야로 4종 정리 확인
삭제 확인창에 영향 범위 연결된 구성원·카테고리 수를 미리 조회해 명시. 못 읽으면 그 사실을 알림 운영 화면 확인 대기
삭제된 업무분야 연결 표시 230 같은 숫자 대신 ⚠ 삭제된 업무분야 (번호 N) + 경고 배너 타입체크·린트 통과

배포 중 발견한 별건

운영 백엔드의 systemd 서비스가 NRestarts=72087 — 7만 번 넘게 재기동 실패를 반복하고 있었다. 실제 서빙은 systemd가 관리하지 않는 고아 프로세스가 하고 있었고, systemd는 8080 포트를 못 잡아 Restart=always 로 무한 재시도 중이었다. 이 상태에서는 systemctl restart 가 아무 효과가 없다 — 배포해도 옛 프로세스가 계속 돌아 반영되지 않는다. 정리했고, 지금은 systemd가 정상적으로 관리한다.

2. 핵심 포인트

확정된 사실 (직접 관측)

내가 틀렸다가 정정한 것

1차 오진 — “열어서 저장만 해도 카테고리 매핑이 전부 삭제된다” (확정이라고 단언했음)

매퍼의 getExpertiseDtl 이 카테고리를 안 돌려주는 것만 보고, 그 위 서비스 계층이 board[0].categoryIdx 에 주입하는 코드를 읽지 않았다. 6단계 체인을 그려놓고 확정이라 했는데 첫 단계가 틀렸으니 전부 무너진다.

2차 오진 — “N:M인데 첫 번째만 남아 나머지가 삭제된다”

코드 동작으로는 맞다. 그러나 그 경로가 실제로 발동한 적이 있는지 확인하지 않았다. 운영·로컬 모두 업무분야당 카테고리가 정확히 1개씩이라 영원히 발동하지 않는 경로였다.

3차 오진 — “복원이 한 번도 안 돌았다”

8/19 문서의 “운영 미적용” 기재를 그대로 믿었다. 백업 테이블과 sort_order 일치가 반박했다.

4차 오진 — “로그에 삭제 기록이 없으니 삭제가 아니다”

대조군을 돌려보니 이 앱은 어떤 어드민 엔드포인트도 로그에 남기지 않았다 (updateExpertise·deleteBBS·lawlin/admin 전부 0건). 부재는 증거가 아니었다.

공통 실수는 하나다 — 코드만 읽고 “확정” 이라고 했다. 그 코드가 실제 데이터에서 돌아간 적이 있는지는 보지 않았다. 접근 가능한 DB를 한참 뒤에야 열었다.

코드 문제인가 — 정확한 답

이번 실종의 원인은 코드가 아니다. 다만 반복을 허용한 것은 코드다.

  1. cascade 누락 — 삭제가 고아를 남겼다. 두 번 다 같은 잔해가 생겼다.
  2. 영향 범위 미고지 — 구성원 6명이 걸린 업무분야인데 확인창은 “해당 자료를 삭제하시겠습니까?” 한 줄이었다.
  3. 감사 로그 없음 — 두 번째 삭제의 주체를 못 찾은 이유. 가장 아픈 부분.
  4. 복구 수단 없음 — 지워지면 DB 직접 조작 외에 방법이 없다.
  5. 고아가 화면에 샘 — 담당자 화면에 정체불명의 숫자 230 이 그대로 노출됐다.

아직 열려 있는 판단

보류한 브랜치 2건

항목내용보류 이유
업무분야 카테고리 다중 선택 (프런트) N:M인데 단일 <select> 로 다뤄, 카테고리가 2개 이상이면 저장 시 첫 번째만 남는다. 체크박스 칩 + categoryIdxList 전송 + 목록 로드 실패 시 저장 차단 카테고리 2개 이상인 업무분야가 현재 0건이라 발동하지 않음. 담당자 입력 화면이 바뀜
showset 처리 (백엔드 매퍼) insertExpertiseshowset 을 INSERT하지 않는데 선택 목록은 showset='Y' 만 읽는다. updateExpertise 에도 갱신 경로가 없어 어드민에서 켤 수 없다 ESG 건은 행 자체가 없던 문제라 무관. 다만 showset='N' 인 업무분야 6건이 실제로 막혀 있음

3. 전체 대화 원문

사용자의 요청과 답변을 순서대로 수록했습니다. 실행한 명령과 그 결과 중 판단 근거가 된 것을 함께 담았습니다.

질문 01
프로젝트 파악부터 해
답변

법무법인 린(law-lin.com)의 관리자 SPAlaw-firm-admin. React 18 + TypeScript + Vite 7 + Tailwind 4. 백엔드는 별도 Java 서버(be-lawlin)이고 이 저장소는 프런트엔드만 담습니다.

주요 의존성: react-router-dom 7, axios, ckeditor4-react, dompurify, chart.js, @tanstack/react-query(설치는 되어 있음).

구조 (FSD 변형)

src/
  app/       router.tsx — 전 라우트가 여기 한 파일에 선언
  pages/     화면 32개 (실질 코드의 대부분)
  widgets/   layout / sidebar / header
  entities/  board/ 하나뿐 — api.ts(1,550줄)에 게시판·구성원·업무분야·팝업 API가 전부 몰려 있음
  shared/    config(엔드포인트 전량 하드코딩), lib(axios·auth·sanitize-html), ui(editor·table·button…)

기능 영역: 사이트 설정(팝업 PC/모바일, 배너, 면책·개인정보·이용안내, 사이트코드) · 업무분야 · 전문분야 카테고리 · 구성원 · 게시판(/board/:lang 다국어) · 인재채용(+지원서) · DB 리스트(뉴스/구독/상담/문의) · 관리자 계정 · 접속 통계.

인증은 jToken 을 sessionStorage에 두고(localStorage에 있던 레거시 토큰은 자동 이관) axios 인터셉터가 헤더를 붙이며, 401/403이면 /login 으로 강제 이동합니다.

이 프로젝트의 실질적 중심은 에디터입니다

최근 커밋 20개 중 대부분이 에디터입니다 — 워드 붙여넣기 글자 크기 유실, 붙여넣은 base64 이미지 서버 업로드, 이중 업로드, 리스트 Shift+Enter(한 번 고쳤다 revert됨).

CKEditor 4.22.1을 저장소에 통째로 넣어 쓰고 있고(2023-06 EOL), 출력단은 sanitize-html.ts(DOMPurify)로 막아둔 상태입니다. develop 브랜치에 CKEditor 5 시험 적용이 올라가 있습니다 — CK4/CK5 런타임 공존, ?editor=ck4로 즉시 롤백, 손실 감지 배너까지 구현·검증 완료. 남은 판단 두 가지는 그대로입니다: ① 개발 DB 200건 중 <br> 사용 98%라 전환 시 전체 글 줄간격이 바뀜 ② CK5 라이선스(현재 'GPL'은 시험용). main 에는 아직 CK4만 있습니다.

배포

브랜치동작
developGitHub Actions → 개발서버 자동 배포 + 헬스체크 실패 시 자동 롤백
mainCI 빌드 검증만
실서버npm run deploy 로컬 수동만

실서버 오배포를 3중으로 막아뒀습니다(값 미설정 시 스킵 / 실서버 IP 차단 / CI 키는 개발서버에만 등록).

눈에 걸린 것 세 가지

  1. scripts/deploy.sh:13 에 실서버 root 비밀번호가 기본값으로 커밋되어 있습니다. 바로 위 주석은 “기본값을 두지 않는다”고 하고 docs/DEPLOY.md 도 “제거했다”고 적혀 있는데, 커밋 c5c4d6a(“실서버 비밀번호 기본값 복원”)가 되돌려 놓은 상태입니다. 코드·주석·문서가 서로 어긋나 있습니다.
  2. ckeditor/(루트)와 public/ckeditor/ 가 중복 사본입니다. 실제 서빙되는 건 public/ 쪽뿐이고 루트 사본은 이미 달라진 죽은 사본입니다 — 여기를 고치면 반영이 안 됩니다.
  3. README.md 가 Vite 템플릿 그대로고 mainCLAUDE.md 가 없습니다. 문서는 develop 에만 있습니다.

질문 02
ESG ·컴플라이언스 항목 버그 기억 있음?
답변

네, 있습니다. 다만 제 메모리가 아니라 claude-shared 허브에 남아 있던 문서입니다 — lawlin-be-dev__be/docs/esg-compliance-expertise-restore.md, 2026-08-19 작성. 이 admin-lawlin 저장소가 아니라 백엔드 쪽 작업이었습니다.

무슨 일이었나

관리자 화면 업무분야 선택 목록에서 ESG·컴플라이언스 가 사라졌다는 문의. 원인은 코드 버그가 아니라 데이터였습니다 — ESG 조직 명칭 개편 과정에서 카테고리(ec_idx=21)는 ‘탄소기후그룹’으로 개명했는데, 같은 이름의 업무분야(e_idx=230)는 삭제했습니다.

진짜 버그는 그 옆에 있었습니다

deleteExpertisedf_site_expertise 한 줄만 지우고 매핑을 안 치웁니다. 외래키가 하나도 없어서 고아가 그대로 남았습니다 — 구성원 매핑 5건, 카테고리 맵 1건, professional_related_items 5건, ref_expertise CSV 참조 3건.

조치 상태 — 로컬 검증 완료, 운영 미적용

  • 코드: deleteExpertise@Transactional + 매핑 정리 5단계, SQL 쿼리 4개·DAO 메서드 4개 추가
  • 데이터: query/20260819_restore_esg_compliance.sql(멱등) + 롤백 스크립트

그때 남겨둔 판단거리

  1. 이 삭제가 실수인지 아직 확인 안 됐습니다. 조직 개편이면 복원하지 않는 게 맞습니다.
  2. 복원하면 6명이 붙습니다 — 기존 5명 + 송유빈(p_idx=337).
  3. 복원된 업무분야는 개명된 ‘탄소기후그룹’ 카테고리 아래로 들어갑니다.
  4. 별건으로 df_site_professionals_expertisee_idx 가 NULL인 행 20건 — 원인 미확인.

질문 03
또 사라졌는데 코드상에서 문제가 있는건지 검수해
답변

검수를 시작할 수 없었습니다. macOS가 세션의 파일 접근을 차단하고 있었습니다.

claude-shared: OK      home: OK
Desktop:  DENIED       Documents: DENIED

프로젝트가 ~/Desktop/Documents/work/ 아래라 소스를 한 줄도 못 읽는 상태였습니다.

권한 없이도 짚을 것

“또 사라진” 게 아니라 “애초에 안 돌아왔을” 가능성이 높습니다. 8/19 문서 상태가 운영 미적용 이었고, 허브를 8/19 이후로 뒤져도 적용을 기록한 새 문서가 없습니다.

코드를 보기 전에 운영 DB에서 df_site_expertisee_idx=230 행이 있는지만 확인하면 갈림길이 정해집니다.

주석. 이 판단은 뒤에서 뒤집힙니다. 복원은 실제로 실행됐고, 그 뒤 다시 삭제됐습니다.


질문 04
요청해봐
답변

두 가지를 시도했습니다. ① 재시도 — 여전히 DENIED ② 샌드박스를 끄고 재시도 — 역시 DENIED.

샌드박스를 벗겨도 같다는 건 막는 주체가 Claude Code가 아니라 macOS 자체(TCC)라는 뜻입니다. macOS는 한 번 거부된 폴더 권한을 다시 묻지 않습니다 — 그래서 프롬프트가 안 뜨고 조용히 EPERM만 돌아옵니다.

시스템 설정 → 개인정보 보호 및 보안 → 파일 및 폴더에서 데스크탑·서류 폴더를 켜고 앱을 재시작해 달라고 안내했습니다.


질문 05
다시해봐
답변

여전히 DENIED. 대신 어느 앱에 권한을 줘야 하는지를 찾았습니다.

claude ← /bin/zsh ← /usr/bin/login ← /Applications/Orca.app (Orca Helper)

터미널이나 iTerm이 아니라 Orca.app 안에서 돌고 있었습니다. 터미널에 권한을 줬다면 효과가 없습니다.


질문 06
[손쉬운 사용 설정 스크린샷] 이거면 된거 아니냐
답변

아쉽지만 그 화면은 다른 권한이었습니다. 문구가 “아래 응용 프로그램이 사용자의 컴퓨터를 제어하는 것을 허용합니다” — 이건 손쉬운 사용(Accessibility) 이고 키보드·마우스 조작 권한입니다. 파일 읽기와 무관합니다.

필요한 것은 전체 디스크 접근 권한 또는 파일 및 폴더Orca 의 데스크탑·서류 폴더 토글이라고 안내했습니다.


질문 07
해보ㅓㅏ
답변

권한은 여전히 막혀 있었습니다. 다른 길로 우회했습니다gh 가 인증되어 있어 GitHub에서 두 저장소를 scratchpad로 복제했습니다.

그 상태로 검수한 결과를 보고했는데, 여기서 두 번의 오진이 나왔습니다.

  1. 1차 진단(틀림) — “getExpertiseDtl 이 카테고리를 안 돌려주므로, 열어서 저장만 해도 매핑이 전부 삭제된다”. 6단계 체인을 그려 “확정”이라 단언했으나, 서비스 계층이 board[0].categoryIdx 에 주입하는 코드를 읽지 않은 오류였습니다.
  2. 2차 진단(원인 아님) — “N:M인데 첫 번째만 남아 나머지가 삭제된다”. 코드 동작은 맞지만 데이터상 카테고리가 2개 이상인 업무분야가 0건이라 발동하지 않습니다.

이때 함께 보고한 것들 중 유효했던 것: 159a557(cascade 수정)이 origin/main 에 없어 운영에 반영되지 않았다는 사실.


질문 08
권환 요청해봐
답변

거부 기록 자체를 지워 macOS가 다시 묻게 만들었습니다.

tccutil reset SystemPolicyDesktopFolder  com.stablyai.orca
tccutil reset SystemPolicyDocumentsFolder com.stablyai.orca

바로 Desktop: OK 로 바뀌었습니다. 앱 재시작도 필요 없었습니다. (이후 세션 내내 권한이 반복적으로 끊겨, 그때마다 같은 명령으로 복구했습니다.)

로컬 실제 파일로 검수 결과를 재확인했고, 문제 코드가 그대로임을 확인했습니다.


질문 09
순서대로 진행해
답변

작업 중 1차 오진을 정정했습니다. 매퍼는 카테고리를 안 돌려주지만 서비스 계층이 board[0].categoryIdx 에 주입하고 있었습니다(LawlinAdminServiceImpl.java:1027). 넣었던 매퍼 수정은 중복이라 되돌렸습니다.

실제 버그는 서비스가 넣어주는 것이 categoryIdxList.get(0), 즉 첫 번째 하나뿐이고 프런트가 단일 <select> 로 다룬다는 점이었습니다.

프런트를 목록 기반으로 고치고(체크박스 칩, categoryIdxList 전송, 목록 로드 실패 시 저장 차단), 백엔드에는 showset 안전 처리를 넣었습니다. npx tsc -b 통과.


질문 10 (작업 중 추가)
되면 로컬로 확인할 수 있게 해
답변

백엔드 컴파일이 실패했는데 제 변경 이전에도 동일하게 실패하는 것을 확인했습니다. 원인은 JDK 26에서 Lombok이 깨지는 것이었습니다(CI는 17). JDK 8도 invalid target release: 11 로 실패했습니다.

프런트 개발서버를 개발서버 API를 바라보게 띄워 확인 경로를 만들었습니다. 실서버(api.law-lin.com)는 쓰지 않았습니다.


질문 11
openjdk@17 설치해서 백엔드도 빌드해봐
답변

brew install openjdk@17 후 전부 통과했습니다.

  • mvn compile — 성공
  • mvn packagetarget/sht_webapp.jar, BUILD SUCCESS
  • MyBatis 매퍼 파싱 검증 — 빌드는 XML 문법만 보므로 매퍼를 실제 파싱시키는 검사를 따로 작성해 돌렸습니다
호출 조건결과
insertExpertise (showset 미전달)showset 컬럼 포함IFNULL'Y'
updateExpertise (showset='N')showset 반영
updateExpertise (showset 미전달)showset 제외 → NULL 덮어쓰기 없음

세 번째가 핵심이었습니다. 파라미터 키 자체가 없을 때를 대비해 jdbcType=VARCHAR 도 명시했습니다.


질문 12
[vite 5173 스크린샷] /Users/yuseungdo/Desktop/Documents/work/lawlin-be-dev/be 여기 맞아?
답변

정확한 지적이었습니다. 제가 다른 사본을 고치고 있었습니다.

  • lawlin-be-dev/be ← IntelliJ가 실제로 돌리고 있는 곳 (8080에서 dev 프로파일로 기동 중)
  • be-lawlin ← 제가 수정한 곳. 같은 GitHub 저장소의 별개 클론

수정을 올바른 사본으로 옮기고 잘못 건드린 사본은 원상복구했습니다. 프런트는 문제없었습니다 — 5173이 제가 편집한 디렉터리에서 돌고 있어 HMR로 이미 반영돼 있었습니다.

dev 프로파일은 로컬 MySQL(127.0.0.1:3312)을 쓰므로 테스트가 완전히 로컬이라 안전했습니다.


질문 13
[CORS 오류 콘솔 스크린샷]
답변

제 변경과 무관한 문제였습니다. SecurityConfig.java 의 로컬 수정이 실행조차 되지 않는 코드였습니다.

if (origins.isEmpty()) {                                    // ← 절대 참이 되지 않음
    return Arrays.asList("http://localhost:3000, http://localhost:5173");
}                                                            // ← 게다가 콤마 포함 단일 문자열

allowedOrigins@Value("${Globals.Allow.Origin:http://localhost:3000}")기본값이 있어 절대 비지 않으므로 이 분기는 죽은 코드입니다. 설령 실행됐더라도 저 값은 원소 하나짜리 리스트라 어떤 origin과도 매칭되지 않습니다.

진짜 원인application-dev.properties:17Globals.Allow.Origin 목록에 5173만 빠져 있던 것이었습니다. 원상복구 후 올바른 지점에 추가했습니다.

별건으로 Gemini API 키가 평문으로 커밋되어 있는 것을 발견해 보고했습니다(값은 옮기지 않음).


질문 14
재시작했어, 확인해봐
답변

CORS 통과 확인 (Access-Control-Allow-Origin: http://localhost:5173). 그리고 로컬 DB를 실측하면서 2차 오진을 정정했습니다.

업무분야 102건 / 매핑 97행 / 매핑된 업무분야 97건

매핑 행 수와 매핑된 업무분야 수가 같습니다. 이 DB에서 카테고리가 2개 이상 걸린 업무분야는 0건입니다. 따라서 제가 지목한 “첫 번째만 남는다”는 잠재 버그일 뿐 이번 실종의 원인이 아닙니다.

또한 로컬에서는 e_idx=230showset='Y' 로 멀쩡했고 카테고리 이름도 ‘탄소기후그룹’이 아니었습니다 — 로컬 DB가 운영과 상태가 달라 증상 자체가 재현되지 않았습니다.

대신 카테고리 매핑이 없고 전부 showset='N' 인 업무분야 6건(등기·경매, 동유럽·중앙아시아, 해외 SOC, 제조물책임·인증·리콜, 중남미·남유럽, 사모펀드)을 발견했습니다.


질문 15
워크벤치로 접근하려니 터짐
답변

운영 DB가 접속 IP 화이트리스트로 막혀 있었습니다. 이 맥에서도, 개발서버 경유로도 Lost connection at 'reading initial communication packet' 로 끊겼습니다. Workbench가 “터진” 것도 같은 원인입니다.

mysql-client 를 설치하고, 허가를 받아 운영 서버를 경유하는 SSH 터널로 조회했습니다. 운영 서버에는 아무것도 설치하지 않았습니다.

결과 — 원인 확정.

df_site_expertise           e_idx=230  →  0건   ← 업무분야 행 자체가 없음
df_site_expertise_category_map        →  1건   ← 고아
df_site_professionals_expertise       →  6건   ← 고아
df_site_professional_related_items    →  6건   ← 고아
ref_expertise 가 230 을 참조             →  3건   ← 고아

연결 대상 6명도 확인: 박경희·조선희·김종식·강민구·이석·송유빈.


질문 16
덤프 떠와서 로컬 DB 최신화 해
답변

운영에 잠금을 걸지 않도록 --single-transaction --skip-lock-tables 로 읽기만 했고, 로컬 현재 DB를 먼저 백업했습니다.

항목
운영 DBMySQL 8.0.31 / 89테이블 / 약 400MB
로컬 DBMySQL 8.0.46 (docker) — 호환
덤프57M, 89테이블 전부, 정상 종료 확인

적재 후 검증에서 운영 상태가 그대로 재현됐습니다: 테이블 89 / 업무분야 102 / e_idx=230 행 0건 / 고아 1·6·6건 / ec_idx=21 이름 ‘탄소기후그룹’. 한글 인코딩(가운뎃점 포함)도 정상이었습니다.


질문 17
결국 문제는 코드 문제냐
답변

아니요. df_site_expertise 에서 행을 지우는 SQL은 딱 하나이고 호출 경로도 하나뿐입니다. 다른 어떤 코드도 이 행을 지우지 않습니다.

코드 문제는 세 가지가 있는데 전부 “원인”이 아니라 “피해를 키우거나 복구를 막은” 것들입니다 — cascade 누락, 삭제 경고 없음, 복구 수단 없음.

그리고 앞서 지목했던 두 건(카테고리 N:M, showset)은 이번 건과 무관한 예방 수정이라고 정정했습니다.


질문 18
아까는 코드 문제라며
답변

네, 그렇게 말했고 두 번 다 틀렸습니다. 1차는 서비스 계층을 안 읽고 “확정”이라 단언한 것, 2차는 코드 경로가 실제로 발동한 적 있는지 확인하지 않은 것입니다. 공통 실수는 코드만 읽고 데이터를 보지 않은 것입니다.

그리고 방금 한 주장에도 어긋나는 데이터가 나왔다고 스스로 밝혔습니다.

항목8/19 문서당시 운영
professionals_expertise5건6건 (+ 송유빈)
professional_related_items5건6건
카테고리 매핑 / ref1건 / 3건1건 / 3건 (동일)

“복원이 한 번도 안 돌았다”는 안전한 결론이 아니라고 정정했습니다.


질문 19
원인 추적해
답변

증거 네 가지를 모아 확정했습니다.

  1. sort_order 일치 — 복원 스크립트가 하드코딩한 값(18/22/22/30/10/2)이 운영 6명 전부와 일치. 특히 송유빈의 2 는 스크립트가 새로 만든 값이라 우연일 수 없음
  2. 백업 테이블 존재 — 스크립트가 만드는 _bak_*_20260819 3개가 운영 덤프에 이미 있음
  3. 백업 테이블 내용 — 복원 직전 상태가 5명(송유빈 없음)으로 기록됨
  4. 타임스탬프 — 송유빈 행 created_at 8/19 08:58:46 UTC = 17:58 KST. 카테고리 매핑 백업 행은 06:35 UTC = 15:35 KST로, 문서의 “개명 15:19” 및 커밋 시각 18:01과 맞물림

실행 순서가 결론을 확정했습니다. 백업 생성(40~46행) → 업무분야 INSERT(54행) → 매핑 INSERT(64·80·92행) 순인데, 54행 앞뒤가 모두 실행됐습니다. INSERT IGNORE 는 오류가 나도 중단되지 않으므로 54행만 건너뛸 수 없습니다.

롤백 스크립트는 매핑까지 전부 지우는데 매핑이 남아 있으므로 롤백도 실행되지 않았습니다. 남는 경로는 하나 — 복원 이후 누군가 어드민에서 다시 삭제했습니다.

또한 “로그에 삭제 기록이 없다”는 제 앞선 추론도 정정했습니다. 대조군을 돌려보니 이 앱은 어떤 어드민 엔드포인트도 로그에 남기지 않았습니다.


질문 20
[담당자 제보 메일 + 송유빈 관련업무 스크린샷] 송유님 연구윈님 업무 분야 작성 시 ESG ·컴플라이언스 항목을 추가한 것으로 확인했으나, 금일 확인해보니 기존에 존재했었던 ESG ·컴플라이언스가 사라진 것으로 확인됩니다. 또한, 송유빈 연구원님 업무분야에도 ESG ·컴플라이언스 항목 추가 요청드립니다. 반복되는 이슈라서 향후 재발방지를 위한 조치도 요청드립니다.
답변

스크린샷이 결정적이었습니다. “선택 항목 순서”의 2번이 230 — 이름이 아니라 e_idx 원본값입니다. 고아 매핑이 담당자 화면에 그대로 노출되고 있었습니다.

로컬에 복원을 적용해 검증했더니 곧바로 이름을 되찾았습니다.

1  207  ESG/기후테크
2  230  ESG·컴플라이언스   ← 스크린샷의 "230" 자리
3  248  온실가스감축본부

따라서 “송유빈에게 추가해달라”는 요청은 별도 작업이 아니었습니다. 본체만 되살리면 이미 있는 연결이 자동으로 이름을 되찾습니다.

또한 230 이라는 숫자가 담당자 화면에 노출되는 것 자체가 버그라고 지적했습니다.


질문 21
복원부터 진행해
답변

단계별로 진행했습니다.

  1. 터널 열고 적용 전 상태 기록 (행 0, 고아 6/1/6/3)
  2. 관련 5개 테이블 별도 백업 — 스크립트 내장 백업은 8/19 것이라 오늘 상태를 담지 못하므로
  3. 복원 스크립트 적용
  4. 독립 재검증
  5. 터널 닫음
검증결과
선택 목록 노출230 ESG·컴플라이언스
송유빈 관련업무230 → 이름 정상 표시
이름 안 붙는 고아0건 (전체 기준)
연결된 구성원6명

백업 파일 2종을 남겼고, 운영에는 조회와 이번 복원 외 아무 변경도 하지 않았습니다.


질문 22
재발 방지 조치하고 확인 후 배포
답변

네 가지를 만들고 각각 실측 검증했습니다.

1) 어드민 변경 감사 로그 (신규)

/lawlin/admin/** 인터셉터. 조회는 제외하고 변경만 기록합니다. 별도 테이블 없이 기존 logback 파일(월별 보관)에 남기고, root 레벨이 나중에 올라가도 이력이 사라지지 않도록 ADMIN_AUDIT 을 INFO로 고정했습니다.

로컬에 임시 관리자를 만들어 토큰을 발급받아 검증했습니다.

ADMIN_AUDIT : actor=audittest ip=... action=deleteExpertise params={eIdx=999999} status=200

조회는 남지 않고 변경만 남는 것을 확인했습니다. 테스트 계정·데이터는 모두 삭제했습니다.

2) cascade 정리

테스트 업무분야에 매핑 4종을 심고 삭제 API를 호출해 4종 전부 정리, ref_expertise 는 해당 번호만 제거(230,207 보존)되는 것을 확인했습니다.

3) 삭제 확인창에 영향 범위

삭제 전 getExpertiseDtl 로 연결된 구성원·카테고리 수를 읽어 명시합니다. 영향 범위를 못 읽으면 그 사실을 알립니다 — 조용히 지우지 않습니다.

4) 삭제된 업무분야 연결 표시

230 대신 ⚠ 삭제된 업무분야 (번호 N) 로 표시하고 경고 배너를 띄웁니다.

분리 배포

사용자 결정에 따라 카테고리 다중 선택 UI와 showset분리하고 재발방지 4건만 배포했습니다.

배포 결과

대상상태
프런트 (admin.law-lin.com)새 번들 서빙 확인, 200
백엔드 (api.law-lin.com)jar md5 로컬과 일치, 서비스 active
감사 인터셉터jar 포함 + 운영 로그에 실제 기록 확인
showset (분리 대상)포함 안 됨 — 의도대로

배포 중 systemd 무한 재기동 실패(NRestarts=72087) 를 발견해 정리했습니다. 이 상태에서는 systemctl restart 가 아무 효과가 없어 배포해도 반영되지 않습니다.

중간에 서버에 unzip 이 없어 검증이 0으로 나온 오경보가 있었고, md5 대조와 python3 로 재확인해 정상임을 확정했습니다.


질문 23
누군가가 지웠다고 했잖아? 어느 IP에서 제거 했는지 알 수 있어?
답변

제가 심은 감사 로그는 오늘부터라 과거 삭제는 안 잡힙니다. 그러나 nginx 접근 로그가 8/19까지 남아 있었습니다 — 필요한 구간 전체가 커버됐습니다.

13/Aug/2026:11:54:44  POST /lawlin/admin/deleteExpertise  220.117.***.**
19/Aug/2026:15:38:46  POST /lawlin/admin/deleteExpertise  220.117.***.**
21/Aug/2026:16:53:14  POST /lawlin/admin/deleteExpertise  220.117.***.**   ← 복원 이후

전부 같은 IP, 같은 브라우저(Windows Edge 151), https://admin.law-lin.com/ 에서 발생했습니다. 이 IP는 오늘 감사 로그의 actor=admin 과 동일합니다.

세션 흐름 — 실수로 보입니다

16:52:12  로그인
16:52:15  전문분야 카테고리 화면 조회
16:52:21  업무분야 목록
16:52:35  업무분야 상세 열기 (getExpertiseDtl)
16:52:49  수정 저장 (updateExpertise)
16:53:09  목록으로 복귀
16:53:14  삭제 ← 여기
16:53:15  목록 갱신

로그인 62초 만에 조회 → 수정 → 삭제까지 끝났습니다.

확실한 것과 추정인 것

  • 확실 — 세 번의 요청 시각·IP·브라우저 (nginx 원본 로그)
  • 추정 — 8/21 건이 e_idx=230 을 지운 것. nginx는 요청 본문을 기록하지 않으나, 복원 이후 deleteExpertise 호출은 이 한 건뿐이라 다른 가능성이 없음
  • 모름 — 어느 계정인지. 계정이 공용(admin)으로 보여 개인 특정은 어려움

질문 24
보류한 브랜치 2건이 뭐지
답변

먼저 짚을 것 — 브랜치로 남아 있지 않았습니다. 두 브랜치는 이름만 남고 커밋이 없으며, 실제 내용은 /tmp 패치 파일 두 개에만 있어 재부팅 시 유실될 상태였습니다.

내용은 위 “보류한 브랜치 2건” 표와 같습니다 — ① 업무분야 카테고리 다중 선택(프런트, +122/-24) ② showset 처리(백엔드 매퍼, 2곳).

두 건 모두 검증은 끝난 상태이며, 유실 방지를 위해 브랜치로 커밋해둘지 물었습니다.


질문 25
클라우드플레어 13번지메일로 로그인했다 cli, mcp연동 후 pages혹은 workers 자동 연동해서 내가 별도 요청하면 정적인 페이지로 내가 질문한거하고 니가 답변한거 보내줘 누락없이. 그리고 포인트와 요약본도 보내줘
답변

연동을 준비했습니다. wrangler 4.125.0 확인, Cloudflare MCP 3개(cloudflare-docs·cloudflare-bindings·cloudflare-observability) 등록, Pages 프로젝트 스캐폴드 생성.

wrangler login 은 브라우저 OAuth라 직접 실행을 요청드렸습니다.

Workers가 아니라 Pages를 택한 이유는 정적 HTML만 올리면 되는 용도라 빌드·런타임이 필요 없고 배포가 한 줄로 끝나기 때문입니다.


질문 26
! npx wrangler login → Successfully logged in.
답변

javaoraclecoffee13@gmail.com 계정 연결 확인. Pages 프로젝트 lawlin-transcript 생성 후 첫 배포로 고정 주소를 확보했습니다.

https://lawlin-transcript.pages.dev  →  200

질문 27
내가 별도 요청하면 정적인 페이지로 내가 질문한거하고 니가 답변한거 보내줘 누락없이. 그리고 포인트와 요약본도 보내줘
답변

상시 지시로 메모리에 등록했습니다 — 다음 세션에도 유지됩니다. 기본은 마스킹으로 하되, “마스킹 없이”라고 하시면 원문 그대로 올립니다.


질문 28
지금 올려줘
답변

이 페이지입니다. 요약본·핵심 포인트·전체 대화 원문을 담아 https://lawlin-transcript.pages.dev 로 게시했습니다.


부록 — 남은 작업

부록 — 담당자 회신용 문안

ESG·컴플라이언스 업무분야를 복구했습니다. 송유빈 연구원님 업무분야에도 정상 표시됩니다(기존 연결이 남아 있어 별도 추가 없이 함께 복구됐습니다). 구성원 6명 연결과 관련업무 참조도 함께 복원했습니다.

확인 결과 8/21 16:53에 어드민 화면에서 해당 업무분야가 삭제된 기록이 있었습니다(사무실 IP). 카테고리 정리 작업 중 발생한 것으로 보입니다.

재발방지로 ① 삭제 시 함께 끊기는 구성원·카테고리 수를 확인창에 표시 ② 삭제 시 연결 데이터 자동 정리 ③ 관리자 변경 작업 이력 기록을 적용했습니다. 앞으로 유사 사례 발생 시 원인 추적이 가능합니다.

작성 2026-08-25 · 재배포 시 이 주소가 최신본을 가리킨다.