v4.3.38: 법률 어시스턴트 앱에 사건(사건 기록) 관리 기능 추가

- 채팅창 파일 첨부 버튼 추가 (PDF/TXT, 회계사 앱과 같은 /api/upload/file 재사용,
  pdf_read로 계약서 분석 — 스킬 프롬프트엔 이미 방법론이 있었는데 버튼만 없었음)
- 탐정 앱의 "사건" 관리를 법률 앱에 이식: 사건 목록(검색/정렬) + 사건별 메모·
  체크리스트·타임라인·첨부자료·제출서류 탭 + 사건별 독립 AI 대화 세션
  (다른 사건 컨텍스트가 안 섞임, 탐정 앱과 동일 패턴)
- PDF 편집기(Stirling-PDF)·Collabora 문서 편집 연동도 그대로 이식 — 사건 폴더에
  자동 저장됨
- 백엔드는 탐정 앱 기존 코드를 건드리지 않고 새 공용 모듈(case-storage.ts)로
  일반화 — appType 파라미터로 detective/lawyer 둘 다 같은 로직 재사용,
  탐정 앱 데이터·동작은 그대로 유지(회귀 테스트로 확인)
- 사진 자동 날짜인식(탐정 앱 전용, 카톡 캡처 등에서 날짜 추출) 등 법률 사건과
  무관한 기능은 이식 범위에서 제외

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-07-30 17:34:52 +09:00
co-authored by Claude Opus 5
parent b61fcd1889
commit 1c2c5e23bc
4 changed files with 1421 additions and 24 deletions
+19 -7
View File
@@ -81,7 +81,7 @@ import { registerUserAppSessionRoutes } from './routes/routes-user-app-sessions'
import { registerChannelMappingRoutes } from './routes/routes-channel-mappings';
import { registerAdminSessionRoutes } from './routes/routes-admin-sessions';
import { registerUserRoutes } from './routes/routes-users';
import { registerDetectiveRoutes, detectiveFilesDir, resolveDetectiveUploadPath } from './routes/routes-detective';
import { registerDetectiveRoutes } from './routes/routes-detective';
import { registerPptxRoutes } from './routes/routes-pptx';
import { registerVoiceRoutes } from './routes/routes-voice';
import { registerVoiceRealtimeRoutes, attachVoiceRealtimeWsProxy } from './routes/routes-voice-realtime';
@@ -91,6 +91,8 @@ import { registerLanguageRoutes } from './routes/routes-language';
import { registerWriterRoutes } from './routes/routes-writer';
import { registerDentalRoutes } from './routes/routes-dental';
import { registerDetectiveDateRoutes } from './routes/routes-detective-dates';
import { registerLawyerCaseRoutes } from './routes/routes-lawyer-cases';
import { caseFilesDir, resolveUploadPath, registerWopiRoutes } from './routes/case-storage';
import {
getConfig,
getAgents,
@@ -2676,12 +2678,16 @@ app.use('/wavacity', express.static(path.join(webUiPath, 'wavacity'), {
// URL with no query string. (Stirling-PDF's root path has a bug where a
// query string on the bare context-path root causes a broken redirect —
// see /pdf-editor below — so the case id is never sent in that request.)
// Cookie value is "<appType>:<caseId>" so the same Stirling-PDF instance and
// auto-save logic serve any case-backed app (detective, lawyer, ...) — app
// defaults to 'detective' when omitted, for links that predate the lawyer app.
app.get('/pdf-editor-open', (req: express.Request, res: express.Response) => {
const session = getSessionUser(req);
if (!session) { res.redirect('/html/login.html'); return; }
const caseId = String(req.query.caseId || '');
if (/^[a-zA-Z0-9_-]+$/.test(caseId)) {
res.setHeader('Set-Cookie', `dt_pdf_case=${caseId}; Path=/pdf-editor; HttpOnly; SameSite=Lax; Max-Age=3600`);
const appType = String(req.query.app || 'detective');
if (/^[a-zA-Z0-9_-]+$/.test(caseId) && /^[a-z]+$/.test(appType)) {
res.setHeader('Set-Cookie', `dt_pdf_case=${appType}:${caseId}; Path=/pdf-editor; HttpOnly; SameSite=Lax; Max-Age=3600`);
}
res.redirect('/pdf-editor/');
});
@@ -2697,7 +2703,11 @@ app.get('/pdf-editor-open', (req: express.Request, res: express.Response) => {
// the browser as normal, so "download" in the editor also lands in the case.
app.use('/pdf-editor', (req: express.Request, res: express.Response) => {
const cookies = parseCookies(req);
const caseId = cookies['dt_pdf_case'] || '';
const rawCookie = cookies['dt_pdf_case'] || '';
const colonIdx = rawCookie.indexOf(':');
// No colon = old-format cookie set before app-scoping existed — treat as detective.
const appType = colonIdx >= 0 ? rawCookie.slice(0, colonIdx) : 'detective';
const caseId = colonIdx >= 0 ? rawCookie.slice(colonIdx + 1) : rawCookie;
const session = getSessionUser(req);
const proxyReq = http.request({
@@ -2721,16 +2731,16 @@ app.use('/pdf-editor', (req: express.Request, res: express.Response) => {
proxyRes.on('end', () => {
const body = Buffer.concat(chunks);
try {
const dir = detectiveFilesDir(session!.username, caseId);
const dir = caseFilesDir(session!.username, appType, caseId);
fs.mkdirSync(dir, { recursive: true });
let filename = 'edited.pdf';
const fnStar = cd.match(/filename\*=UTF-8''([^;]+)/i);
const fnPlain = cd.match(/filename="?([^";]+)"?/i);
if (fnStar) { try { filename = decodeURIComponent(fnStar[1]); } catch {} }
else if (fnPlain) filename = fnPlain[1];
const { relPath, absPath } = resolveDetectiveUploadPath(dir, filename);
const { relPath, absPath } = resolveUploadPath(dir, filename);
fs.writeFileSync(absPath, body);
console.log(`[pdf-editor] saved result to case ${caseId}: ${relPath}`);
console.log(`[pdf-editor] saved result to ${appType} case ${caseId}: ${relPath}`);
} catch (err) {
console.error('[pdf-editor] auto-save failed:', err);
}
@@ -4357,6 +4367,8 @@ registerUserRoutes(app, {
CONFIG_DIR_PATH,
});
registerDetectiveRoutes(app, getSessionUser, isPathInsideDir, CONFIG_DIR_PATH);
registerLawyerCaseRoutes(app, getSessionUser, isPathInsideDir);
registerWopiRoutes(app, '/api/lawyer');
// Redirect legacy top-level HTML routes into /html/ after web-ui reorganization.