*** 로 가렸습니다.
업무 맥락상 필요한 구성원 실명은 그대로 두었습니다. 이 페이지는 검색엔진에 노출되지 않으나(noindex),
주소를 아는 사람은 열람할 수 있습니다.
코드가 지운 것이 아니라 사람이 어드민 삭제 버튼으로 두 번 지웠고, 8/19에 한 번 복구했으나 8/21에 다시 지워진 것이다. 감사 기록이 없어 그동안 추적이 불가능했다. 복구를 마쳤고, 재발방지 조치 네 가지를 운영에 배포했다.
| 시각 | 사건 | 근거 |
|---|---|---|
| 8/13 11:54:44 | deleteExpertise 호출 (이번 건 이전) | nginx 접근 로그 |
| 8/19 15:19~15:35 | ESG 조직 개편 — 카테고리 ec_idx=21 을 ‘탄소기후그룹’ 으로 개명 | 백업 테이블 created_at |
| 8/19 15:38:46 | 1차 삭제 — 업무분야 e_idx=230 삭제 | nginx 접근 로그 |
| 8/19 17:58:46 | 복원 스크립트 운영 실행 — 행 + 구성원 6명 연결 복구 | 송유빈 행 created_at, 백업 테이블 3개 존재 |
| 8/19 18:01 | 커밋 159a557 (문서에는 “운영 미적용” 으로 잘못 기재) | git |
| 8/21 16:53:14 | 2차 삭제 — 다시 삭제됨. 담당자가 “또 사라졌다” 고 제보한 그것 | nginx 접근 로그 |
| 8/25 16:1x | 운영 복원 완료 | 본 작업 |
| 8/25 16:3x | 재발방지 조치 운영 배포 완료 | 본 작업 |
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명 (박경희·조선희·김종식·강민구·이석·송유빈) |
송유빈 화면의 230 | ESG·컴플라이언스 로 정상 표시 |
| 이름 안 붙는 고아 | 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가 정상적으로 관리한다.
df_site_expertise 에서 행을 지우는 SQL은 단 하나(where e_idx = #{eIdx}, 단일 행)이고, 호출 경로도 어드민 삭제 버튼 하나뿐이다.eIdx 를 그대로 넘긴다 — 엉뚱한 행이 지워질 여지가 없다. 일괄 삭제도, 인덱스 기반 조회도 없다./lawlin/admin/** 는 authenticated() — 무토큰 호출은 401 (실측 확인).e_idx=230 행이 없었고, 고아 매핑은 카테고리 1 · 구성원 6 · related_items 6 · ref 3건이 남아 있었다.sort_order(18/22/22/30/10/2)가 6명 전부 일치. 특히 송유빈의 2 는 스크립트가 새로 만든 값이라 우연히 겹칠 수 없다.1차 오진 — “열어서 저장만 해도 카테고리 매핑이 전부 삭제된다” (확정이라고 단언했음)
매퍼의 getExpertiseDtl 이 카테고리를 안 돌려주는 것만 보고, 그 위 서비스 계층이
board[0].categoryIdx 에 주입하는 코드를 읽지 않았다. 6단계 체인을 그려놓고 확정이라 했는데
첫 단계가 틀렸으니 전부 무너진다.
2차 오진 — “N:M인데 첫 번째만 남아 나머지가 삭제된다”
코드 동작으로는 맞다. 그러나 그 경로가 실제로 발동한 적이 있는지 확인하지 않았다. 운영·로컬 모두 업무분야당 카테고리가 정확히 1개씩이라 영원히 발동하지 않는 경로였다.
3차 오진 — “복원이 한 번도 안 돌았다”
8/19 문서의 “운영 미적용” 기재를 그대로 믿었다. 백업 테이블과 sort_order 일치가 반박했다.
4차 오진 — “로그에 삭제 기록이 없으니 삭제가 아니다”
대조군을 돌려보니 이 앱은 어떤 어드민 엔드포인트도 로그에 남기지 않았다
(updateExpertise·deleteBBS·lawlin/admin 전부 0건). 부재는 증거가 아니었다.
공통 실수는 하나다 — 코드만 읽고 “확정” 이라고 했다. 그 코드가 실제 데이터에서 돌아간 적이 있는지는 보지 않았다. 접근 가능한 DB를 한참 뒤에야 열었다.
이번 실종의 원인은 코드가 아니다. 다만 반복을 허용한 것은 코드다.
230 이 그대로 노출됐다.admin)이라 “사무실에서 admin 계정으로” 까지만 알 수 있다. 담당자별 계정 분리가 필요하다.application-dev.properties 에 Gemini API 키가 평문 커밋돼 있고, 배포 가이드 문서에도 서버·DB 비밀번호가 평문으로 적혀 있다. 환경변수/주입으로 빼야 한다.scripts/deploy.sh 에 실서버 비밀번호가 기본값으로 커밋돼 있는데, 바로 위 주석과 docs/DEPLOY.md 는 “제거했다” 고 적혀 있어 서로 어긋난다.| 항목 | 내용 | 보류 이유 |
|---|---|---|
| 업무분야 카테고리 다중 선택 (프런트) | N:M인데 단일 <select> 로 다뤄, 카테고리가 2개 이상이면 저장 시 첫 번째만 남는다. 체크박스 칩 + categoryIdxList 전송 + 목록 로드 실패 시 저장 차단 |
카테고리 2개 이상인 업무분야가 현재 0건이라 발동하지 않음. 담당자 입력 화면이 바뀜 |
showset 처리 (백엔드 매퍼) |
insertExpertise 가 showset 을 INSERT하지 않는데 선택 목록은 showset='Y' 만 읽는다. updateExpertise 에도 갱신 경로가 없어 어드민에서 켤 수 없다 |
ESG 건은 행 자체가 없던 문제라 무관. 다만 showset='N' 인 업무분야 6건이 실제로 막혀 있음 |
사용자의 요청과 답변을 순서대로 수록했습니다. 실행한 명령과 그 결과 중 판단 근거가 된 것을 함께 담았습니다.
법무법인 린(law-lin.com)의 관리자 SPA — law-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(설치는 되어 있음).
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만 있습니다.
| 브랜치 | 동작 |
|---|---|
develop | GitHub Actions → 개발서버 자동 배포 + 헬스체크 실패 시 자동 롤백 |
main | CI 빌드 검증만 |
| 실서버 | npm run deploy 로컬 수동만 |
실서버 오배포를 3중으로 막아뒀습니다(값 미설정 시 스킵 / 실서버 IP 차단 / CI 키는 개발서버에만 등록).
scripts/deploy.sh:13 에 실서버 root 비밀번호가 기본값으로 커밋되어 있습니다. 바로 위 주석은 “기본값을 두지 않는다”고 하고 docs/DEPLOY.md 도 “제거했다”고 적혀 있는데, 커밋 c5c4d6a(“실서버 비밀번호 기본값 복원”)가 되돌려 놓은 상태입니다. 코드·주석·문서가 서로 어긋나 있습니다.ckeditor/(루트)와 public/ckeditor/ 가 중복 사본입니다. 실제 서빙되는 건 public/ 쪽뿐이고 루트 사본은 이미 달라진 죽은 사본입니다 — 여기를 고치면 반영이 안 됩니다.README.md 가 Vite 템플릿 그대로고 main 에 CLAUDE.md 가 없습니다. 문서는 develop 에만 있습니다.네, 있습니다. 다만 제 메모리가 아니라 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)는 삭제했습니다.
deleteExpertise 가 df_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(멱등) + 롤백 스크립트p_idx=337).df_site_professionals_expertise 에 e_idx 가 NULL인 행 20건 — 원인 미확인.검수를 시작할 수 없었습니다. macOS가 세션의 파일 접근을 차단하고 있었습니다.
claude-shared: OK home: OK
Desktop: DENIED Documents: DENIED
프로젝트가 ~/Desktop/Documents/work/ 아래라 소스를 한 줄도 못 읽는 상태였습니다.
“또 사라진” 게 아니라 “애초에 안 돌아왔을” 가능성이 높습니다. 8/19 문서 상태가 운영 미적용 이었고, 허브를 8/19 이후로 뒤져도 적용을 기록한 새 문서가 없습니다.
코드를 보기 전에 운영 DB에서 df_site_expertise 의 e_idx=230 행이 있는지만 확인하면 갈림길이 정해집니다.
주석. 이 판단은 뒤에서 뒤집힙니다. 복원은 실제로 실행됐고, 그 뒤 다시 삭제됐습니다.
두 가지를 시도했습니다. ① 재시도 — 여전히 DENIED ② 샌드박스를 끄고 재시도 — 역시 DENIED.
샌드박스를 벗겨도 같다는 건 막는 주체가 Claude Code가 아니라 macOS 자체(TCC)라는 뜻입니다. macOS는 한 번 거부된 폴더 권한을 다시 묻지 않습니다 — 그래서 프롬프트가 안 뜨고 조용히 EPERM만 돌아옵니다.
시스템 설정 → 개인정보 보호 및 보안 → 파일 및 폴더에서 데스크탑·서류 폴더를 켜고 앱을 재시작해 달라고 안내했습니다.
여전히 DENIED. 대신 어느 앱에 권한을 줘야 하는지를 찾았습니다.
claude ← /bin/zsh ← /usr/bin/login ← /Applications/Orca.app (Orca Helper)
터미널이나 iTerm이 아니라 Orca.app 안에서 돌고 있었습니다. 터미널에 권한을 줬다면 효과가 없습니다.
아쉽지만 그 화면은 다른 권한이었습니다. 문구가 “아래 응용 프로그램이 사용자의 컴퓨터를 제어하는 것을 허용합니다” — 이건 손쉬운 사용(Accessibility) 이고 키보드·마우스 조작 권한입니다. 파일 읽기와 무관합니다.
필요한 것은 전체 디스크 접근 권한 또는 파일 및 폴더 → Orca 의 데스크탑·서류 폴더 토글이라고 안내했습니다.
권한은 여전히 막혀 있었습니다. 다른 길로 우회했습니다 — gh 가 인증되어 있어 GitHub에서 두 저장소를 scratchpad로 복제했습니다.
그 상태로 검수한 결과를 보고했는데, 여기서 두 번의 오진이 나왔습니다.
getExpertiseDtl 이 카테고리를 안 돌려주므로, 열어서 저장만 해도 매핑이 전부 삭제된다”. 6단계 체인을 그려 “확정”이라 단언했으나, 서비스 계층이 board[0].categoryIdx 에 주입하는 코드를 읽지 않은 오류였습니다.이때 함께 보고한 것들 중 유효했던 것: 159a557(cascade 수정)이 origin/main 에 없어 운영에 반영되지 않았다는 사실.
거부 기록 자체를 지워 macOS가 다시 묻게 만들었습니다.
tccutil reset SystemPolicyDesktopFolder com.stablyai.orca
tccutil reset SystemPolicyDocumentsFolder com.stablyai.orca
바로 Desktop: OK 로 바뀌었습니다. 앱 재시작도 필요 없었습니다. (이후 세션 내내 권한이 반복적으로 끊겨, 그때마다 같은 명령으로 복구했습니다.)
로컬 실제 파일로 검수 결과를 재확인했고, 문제 코드가 그대로임을 확인했습니다.
작업 중 1차 오진을 정정했습니다. 매퍼는 카테고리를 안 돌려주지만 서비스 계층이 board[0].categoryIdx 에 주입하고 있었습니다(LawlinAdminServiceImpl.java:1027). 넣었던 매퍼 수정은 중복이라 되돌렸습니다.
실제 버그는 서비스가 넣어주는 것이 categoryIdxList.get(0), 즉 첫 번째 하나뿐이고 프런트가 단일 <select> 로 다룬다는 점이었습니다.
프런트를 목록 기반으로 고치고(체크박스 칩, categoryIdxList 전송, 목록 로드 실패 시 저장 차단), 백엔드에는 showset 안전 처리를 넣었습니다. npx tsc -b 통과.
백엔드 컴파일이 실패했는데 제 변경 이전에도 동일하게 실패하는 것을 확인했습니다. 원인은 JDK 26에서 Lombok이 깨지는 것이었습니다(CI는 17). JDK 8도 invalid target release: 11 로 실패했습니다.
프런트 개발서버를 개발서버 API를 바라보게 띄워 확인 경로를 만들었습니다. 실서버(api.law-lin.com)는 쓰지 않았습니다.
brew install openjdk@17 후 전부 통과했습니다.
mvn compile — 성공mvn package — target/sht_webapp.jar, BUILD SUCCESS| 호출 조건 | 결과 |
|---|---|
insertExpertise (showset 미전달) | showset 컬럼 포함 → IFNULL 로 'Y' |
updateExpertise (showset='N') | showset 반영 |
updateExpertise (showset 미전달) | showset 제외 → NULL 덮어쓰기 없음 |
세 번째가 핵심이었습니다. 파라미터 키 자체가 없을 때를 대비해 jdbcType=VARCHAR 도 명시했습니다.
정확한 지적이었습니다. 제가 다른 사본을 고치고 있었습니다.
lawlin-be-dev/be ← IntelliJ가 실제로 돌리고 있는 곳 (8080에서 dev 프로파일로 기동 중)be-lawlin ← 제가 수정한 곳. 같은 GitHub 저장소의 별개 클론수정을 올바른 사본으로 옮기고 잘못 건드린 사본은 원상복구했습니다. 프런트는 문제없었습니다 — 5173이 제가 편집한 디렉터리에서 돌고 있어 HMR로 이미 반영돼 있었습니다.
dev 프로파일은 로컬 MySQL(127.0.0.1:3312)을 쓰므로 테스트가 완전히 로컬이라 안전했습니다.
제 변경과 무관한 문제였습니다. 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:17 의 Globals.Allow.Origin 목록에 5173만 빠져 있던 것이었습니다. 원상복구 후 올바른 지점에 추가했습니다.
별건으로 Gemini API 키가 평문으로 커밋되어 있는 것을 발견해 보고했습니다(값은 옮기지 않음).
CORS 통과 확인 (Access-Control-Allow-Origin: http://localhost:5173). 그리고 로컬 DB를 실측하면서 2차 오진을 정정했습니다.
업무분야 102건 / 매핑 97행 / 매핑된 업무분야 97건
매핑 행 수와 매핑된 업무분야 수가 같습니다. 이 DB에서 카테고리가 2개 이상 걸린 업무분야는 0건입니다. 따라서 제가 지목한 “첫 번째만 남는다”는 잠재 버그일 뿐 이번 실종의 원인이 아닙니다.
또한 로컬에서는 e_idx=230 이 showset='Y' 로 멀쩡했고 카테고리 이름도 ‘탄소기후그룹’이 아니었습니다 — 로컬 DB가 운영과 상태가 달라 증상 자체가 재현되지 않았습니다.
대신 카테고리 매핑이 없고 전부 showset='N' 인 업무분야 6건(등기·경매, 동유럽·중앙아시아, 해외 SOC, 제조물책임·인증·리콜, 중남미·남유럽, 사모펀드)을 발견했습니다.
운영 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명도 확인: 박경희·조선희·김종식·강민구·이석·송유빈.
운영에 잠금을 걸지 않도록 --single-transaction --skip-lock-tables 로 읽기만 했고, 로컬 현재 DB를 먼저 백업했습니다.
| 항목 | 값 |
|---|---|
| 운영 DB | MySQL 8.0.31 / 89테이블 / 약 400MB |
| 로컬 DB | MySQL 8.0.46 (docker) — 호환 |
| 덤프 | 57M, 89테이블 전부, 정상 종료 확인 |
적재 후 검증에서 운영 상태가 그대로 재현됐습니다: 테이블 89 / 업무분야 102 / e_idx=230 행 0건 / 고아 1·6·6건 / ec_idx=21 이름 ‘탄소기후그룹’. 한글 인코딩(가운뎃점 포함)도 정상이었습니다.
아니요. df_site_expertise 에서 행을 지우는 SQL은 딱 하나이고 호출 경로도 하나뿐입니다. 다른 어떤 코드도 이 행을 지우지 않습니다.
코드 문제는 세 가지가 있는데 전부 “원인”이 아니라 “피해를 키우거나 복구를 막은” 것들입니다 — cascade 누락, 삭제 경고 없음, 복구 수단 없음.
그리고 앞서 지목했던 두 건(카테고리 N:M, showset)은 이번 건과 무관한 예방 수정이라고 정정했습니다.
네, 그렇게 말했고 두 번 다 틀렸습니다. 1차는 서비스 계층을 안 읽고 “확정”이라 단언한 것, 2차는 코드 경로가 실제로 발동한 적 있는지 확인하지 않은 것입니다. 공통 실수는 코드만 읽고 데이터를 보지 않은 것입니다.
그리고 방금 한 주장에도 어긋나는 데이터가 나왔다고 스스로 밝혔습니다.
| 항목 | 8/19 문서 | 당시 운영 |
|---|---|---|
professionals_expertise | 5건 | 6건 (+ 송유빈) |
professional_related_items | 5건 | 6건 |
| 카테고리 매핑 / ref | 1건 / 3건 | 1건 / 3건 (동일) |
“복원이 한 번도 안 돌았다”는 안전한 결론이 아니라고 정정했습니다.
증거 네 가지를 모아 확정했습니다.
sort_order 일치 — 복원 스크립트가 하드코딩한 값(18/22/22/30/10/2)이 운영 6명 전부와 일치. 특히 송유빈의 2 는 스크립트가 새로 만든 값이라 우연일 수 없음_bak_*_20260819 3개가 운영 덤프에 이미 있음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행만 건너뛸 수 없습니다.
롤백 스크립트는 매핑까지 전부 지우는데 매핑이 남아 있으므로 롤백도 실행되지 않았습니다. 남는 경로는 하나 — 복원 이후 누군가 어드민에서 다시 삭제했습니다.
또한 “로그에 삭제 기록이 없다”는 제 앞선 추론도 정정했습니다. 대조군을 돌려보니 이 앱은 어떤 어드민 엔드포인트도 로그에 남기지 않았습니다.
스크린샷이 결정적이었습니다. “선택 항목 순서”의 2번이 230 — 이름이 아니라 e_idx 원본값입니다. 고아 매핑이 담당자 화면에 그대로 노출되고 있었습니다.
로컬에 복원을 적용해 검증했더니 곧바로 이름을 되찾았습니다.
1 207 ESG/기후테크
2 230 ESG·컴플라이언스 ← 스크린샷의 "230" 자리
3 248 온실가스감축본부
따라서 “송유빈에게 추가해달라”는 요청은 별도 작업이 아니었습니다. 본체만 되살리면 이미 있는 연결이 자동으로 이름을 되찾습니다.
또한 230 이라는 숫자가 담당자 화면에 노출되는 것 자체가 버그라고 지적했습니다.
단계별로 진행했습니다.
| 검증 | 결과 |
|---|---|
| 선택 목록 노출 | 230 ESG·컴플라이언스 |
| 송유빈 관련업무 | 230 → 이름 정상 표시 |
| 이름 안 붙는 고아 | 0건 (전체 기준) |
| 연결된 구성원 | 6명 |
백업 파일 2종을 남겼고, 운영에는 조회와 이번 복원 외 아무 변경도 하지 않았습니다.
네 가지를 만들고 각각 실측 검증했습니다.
/lawlin/admin/** 인터셉터. 조회는 제외하고 변경만 기록합니다. 별도 테이블 없이 기존 logback 파일(월별 보관)에 남기고, root 레벨이 나중에 올라가도 이력이 사라지지 않도록 ADMIN_AUDIT 을 INFO로 고정했습니다.
로컬에 임시 관리자를 만들어 토큰을 발급받아 검증했습니다.
ADMIN_AUDIT : actor=audittest ip=... action=deleteExpertise params={eIdx=999999} status=200
조회는 남지 않고 변경만 남는 것을 확인했습니다. 테스트 계정·데이터는 모두 삭제했습니다.
테스트 업무분야에 매핑 4종을 심고 삭제 API를 호출해 4종 전부 정리, ref_expertise 는 해당 번호만 제거(230,207 보존)되는 것을 확인했습니다.
삭제 전 getExpertiseDtl 로 연결된 구성원·카테고리 수를 읽어 명시합니다. 영향 범위를 못 읽으면 그 사실을 알립니다 — 조용히 지우지 않습니다.
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 로 재확인해 정상임을 확정했습니다.
제가 심은 감사 로그는 오늘부터라 과거 삭제는 안 잡힙니다. 그러나 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초 만에 조회 → 수정 → 삭제까지 끝났습니다.
e_idx=230 을 지운 것. nginx는 요청 본문을 기록하지 않으나, 복원 이후 deleteExpertise 호출은 이 한 건뿐이라 다른 가능성이 없음admin)으로 보여 개인 특정은 어려움먼저 짚을 것 — 브랜치로 남아 있지 않았습니다. 두 브랜치는 이름만 남고 커밋이 없으며, 실제 내용은 /tmp 패치 파일 두 개에만 있어 재부팅 시 유실될 상태였습니다.
내용은 위 “보류한 브랜치 2건” 표와 같습니다 — ① 업무분야 카테고리 다중 선택(프런트, +122/-24) ② showset 처리(백엔드 매퍼, 2곳).
두 건 모두 검증은 끝난 상태이며, 유실 방지를 위해 브랜치로 커밋해둘지 물었습니다.
연동을 준비했습니다. wrangler 4.125.0 확인, Cloudflare MCP 3개(cloudflare-docs·cloudflare-bindings·cloudflare-observability) 등록, Pages 프로젝트 스캐폴드 생성.
wrangler login 은 브라우저 OAuth라 직접 실행을 요청드렸습니다.
Workers가 아니라 Pages를 택한 이유는 정적 HTML만 올리면 되는 용도라 빌드·런타임이 필요 없고 배포가 한 줄로 끝나기 때문입니다.
javaoraclecoffee13@gmail.com 계정 연결 확인. Pages 프로젝트 lawlin-transcript 생성 후 첫 배포로 고정 주소를 확보했습니다.
https://lawlin-transcript.pages.dev → 200
상시 지시로 메모리에 등록했습니다 — 다음 세션에도 유지됩니다. 기본은 마스킹으로 하되, “마스킹 없이”라고 하시면 원문 그대로 올립니다.
이 페이지입니다. 요약본·핵심 포인트·전체 대화 원문을 담아 https://lawlin-transcript.pages.dev 로 게시했습니다.
ESG·컴플라이언스 업무분야를 복구했습니다. 송유빈 연구원님 업무분야에도 정상 표시됩니다(기존 연결이 남아 있어 별도 추가 없이 함께 복구됐습니다). 구성원 6명 연결과 관련업무 참조도 함께 복원했습니다.
확인 결과 8/21 16:53에 어드민 화면에서 해당 업무분야가 삭제된 기록이 있었습니다(사무실 IP). 카테고리 정리 작업 중 발생한 것으로 보입니다.
재발방지로 ① 삭제 시 함께 끊기는 구성원·카테고리 수를 확인창에 표시 ② 삭제 시 연결 데이터 자동 정리 ③ 관리자 변경 작업 이력 기록을 적용했습니다. 앞으로 유사 사례 발생 시 원인 추적이 가능합니다.
작성 2026-08-25 · 재배포 시 이 주소가 최신본을 가리킨다.