From 15ca946a007436e2d9539894ce53b30041884056 Mon Sep 17 00:00:00 2001 From: "jeonghyo.k" Date: Tue, 14 Apr 2026 17:11:38 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(prediction):=20GSC=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EC=82=AC=EA=B3=A0=20=EB=AA=A9=EB=A1=9D=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EC=85=80=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=B0=95=EC=8A=A4=20=EC=9E=90=EB=8F=99=20=EC=B1=84=EC=9B=80=20?= =?UTF-8?q?(prediction/hns/rescue)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/gsc/gscAccidentsRouter.ts | 20 +++++ backend/src/gsc/gscAccidentsService.ts | 65 ++++++++++++++++ backend/src/server.ts | 2 + .../src/tabs/hns/components/HNSLeftPanel.tsx | 33 ++++---- frontend/src/tabs/hns/components/HNSView.tsx | 4 + .../tabs/prediction/components/LeftPanel.tsx | 2 + .../prediction/components/OilSpillView.tsx | 3 + .../components/PredictionInputSection.tsx | 47 +++++++++++- .../prediction/components/leftPanelTypes.ts | 2 + .../tabs/prediction/services/predictionApi.ts | 17 +++++ .../src/tabs/rescue/components/RescueView.tsx | 76 +++++++++---------- 11 files changed, 217 insertions(+), 54 deletions(-) create mode 100644 backend/src/gsc/gscAccidentsRouter.ts create mode 100644 backend/src/gsc/gscAccidentsService.ts diff --git a/backend/src/gsc/gscAccidentsRouter.ts b/backend/src/gsc/gscAccidentsRouter.ts new file mode 100644 index 0000000..4ba3cbb --- /dev/null +++ b/backend/src/gsc/gscAccidentsRouter.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import { requireAuth } from '../auth/authMiddleware.js'; +import { listGscAccidents } from './gscAccidentsService.js'; + +const router = Router(); + +// ============================================================ +// GET /api/gsc/accidents — 외부 수집 사고 목록 (최신 20건) +// ============================================================ +router.get('/', requireAuth, async (_req, res) => { + try { + const accidents = await listGscAccidents(20); + res.json(accidents); + } catch (err) { + console.error('[gsc] 사고 목록 조회 오류:', err); + res.status(500).json({ error: '사고 목록 조회 중 오류가 발생했습니다.' }); + } +}); + +export default router; diff --git a/backend/src/gsc/gscAccidentsService.ts b/backend/src/gsc/gscAccidentsService.ts new file mode 100644 index 0000000..69fbff0 --- /dev/null +++ b/backend/src/gsc/gscAccidentsService.ts @@ -0,0 +1,65 @@ +import { wingPool } from '../db/wingDb.js'; + +export interface GscAccidentListItem { + acdntMngNo: string; + pollNm: string; + pollDate: string | null; + lat: number | null; + lon: number | null; +} + +const ACDNT_ASORT_CODES = [ + '055001001', + '055001002', + '055001003', + '055001004', + '055001005', + '055001006', + '055003001', + '055003002', + '055003003', + '055003004', + '055003005', + '055004003', +]; + +export async function listGscAccidents(limit = 20): Promise { + const sql = ` + SELECT DISTINCT ON (a.acdnt_mng_no) + a.acdnt_mng_no AS "acdntMngNo", + a.acdnt_title AS "pollNm", + to_char(a.rcept_dt, 'YYYY-MM-DD"T"HH24:MI') AS "pollDate", + a.rcept_dt AS "rceptDt", + b.la AS "lat", + b.lo AS "lon" + FROM gsc.tgs_acdnt_info AS a + LEFT JOIN gsc.tgs_acdnt_lc AS b + ON a.acdnt_mng_no = b.acdnt_mng_no + WHERE a.acdnt_asort_code = ANY($1::varchar[]) + AND a.acdnt_title IS NOT NULL + ORDER BY a.acdnt_mng_no, b.acdnt_lc_sn ASC + `; + + const orderedSql = ` + SELECT "acdntMngNo", "pollNm", "pollDate", "lat", "lon" + FROM (${sql}) t + ORDER BY t."rceptDt" DESC NULLS LAST + LIMIT $2 + `; + + const result = await wingPool.query<{ + acdntMngNo: string; + pollNm: string; + pollDate: string | null; + lat: string | null; + lon: string | null; + }>(orderedSql, [ACDNT_ASORT_CODES, limit]); + + return result.rows.map((row) => ({ + acdntMngNo: row.acdntMngNo, + pollNm: row.pollNm, + pollDate: row.pollDate, + lat: row.lat != null ? Number(row.lat) : null, + lon: row.lon != null ? Number(row.lon) : null, + })); +} diff --git a/backend/src/server.ts b/backend/src/server.ts index a577c9d..09999dc 100755 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -19,6 +19,7 @@ import hnsRouter from './hns/hnsRouter.js' import reportsRouter from './reports/reportsRouter.js' import assetsRouter from './assets/assetsRouter.js' import incidentsRouter from './incidents/incidentsRouter.js' +import gscAccidentsRouter from './gsc/gscAccidentsRouter.js' import scatRouter from './scat/scatRouter.js' import predictionRouter from './prediction/predictionRouter.js' import aerialRouter from './aerial/aerialRouter.js' @@ -168,6 +169,7 @@ app.use('/api/hns', hnsRouter) app.use('/api/reports', reportsRouter) app.use('/api/assets', assetsRouter) app.use('/api/incidents', incidentsRouter) +app.use('/api/gsc/accidents', gscAccidentsRouter) app.use('/api/scat', scatRouter) app.use('/api/prediction', predictionRouter) app.use('/api/aerial', aerialRouter) diff --git a/frontend/src/tabs/hns/components/HNSLeftPanel.tsx b/frontend/src/tabs/hns/components/HNSLeftPanel.tsx index ed939ac..7822dd8 100755 --- a/frontend/src/tabs/hns/components/HNSLeftPanel.tsx +++ b/frontend/src/tabs/hns/components/HNSLeftPanel.tsx @@ -3,8 +3,8 @@ import { ComboBox } from '@common/components/ui/ComboBox'; import { useWeatherFetch } from '../hooks/useWeatherFetch'; import { getSubstanceToxicity } from '../utils/toxicityData'; import type { WeatherFetchResult, ReleaseType } from '../utils/dispersionTypes'; -import { fetchIncidentsRaw } from '@tabs/incidents/services/incidentsApi'; -import type { IncidentListItem } from '@tabs/incidents/services/incidentsApi'; +import { fetchGscAccidents } from '@tabs/prediction/services/predictionApi'; +import type { GscAccidentListItem } from '@tabs/prediction/services/predictionApi'; /** HNS 분석 입력 파라미터 (부모에 전달) */ export interface HNSInputParams { @@ -44,6 +44,7 @@ interface HNSLeftPanelProps { onParamsChange?: (params: HNSInputParams) => void; onReset?: () => void; loadedParams?: Partial | null; + onFlyToCoord?: (coord: { lon: number; lat: number }) => void; } /** 십진 좌표 → 도분초 변환 */ @@ -67,8 +68,9 @@ export function HNSLeftPanel({ onParamsChange, onReset, loadedParams, + onFlyToCoord, }: HNSLeftPanelProps) { - const [incidents, setIncidents] = useState([]); + const [incidents, setIncidents] = useState([]); const [selectedIncidentSn, setSelectedIncidentSn] = useState(''); const [expandedSections, setExpandedSections] = useState({ accident: true, params: true }); const toggleSection = (key: 'accident' | 'params') => @@ -138,21 +140,26 @@ export function HNSLeftPanel({ // 사고 목록 불러오기 (마운트 시 1회, ref 초기화 패턴) const incidentsPromiseRef = useRef | null>(null); if (incidentsPromiseRef.current == null) { - incidentsPromiseRef.current = fetchIncidentsRaw() + incidentsPromiseRef.current = fetchGscAccidents() .then((data) => setIncidents(data)) .catch(() => setIncidents([])); } // 사고 선택 시 필드 자동 채움 - const handleSelectIncident = (snStr: string) => { - setSelectedIncidentSn(snStr); - const sn = parseInt(snStr); - const incident = incidents.find((i) => i.acdntSn === sn); + const handleSelectIncident = (mngNo: string) => { + setSelectedIncidentSn(mngNo); + const incident = incidents.find((i) => i.acdntMngNo === mngNo); if (!incident) return; - setAccidentName(incident.acdntNm); - if (incident.lat && incident.lng) { - onCoordChange({ lat: incident.lat, lon: incident.lng }); + setAccidentName(incident.pollNm); + if (incident.pollDate) { + const [d, t] = incident.pollDate.split('T'); + if (d) setAccidentDate(d); + if (t) setAccidentTime(t); + } + if (incident.lat != null && incident.lon != null) { + onCoordChange({ lat: incident.lat, lon: incident.lon }); + onFlyToCoord?.({ lat: incident.lat, lon: incident.lon }); } }; @@ -266,8 +273,8 @@ export function HNSLeftPanel({ onChange={handleSelectIncident} placeholder="또는 사고 리스트에서 선택" options={incidents.map((inc) => ({ - value: String(inc.acdntSn), - label: `${inc.acdntNm} (${new Date(inc.occrnDtm).toLocaleDateString('ko-KR')})`, + value: inc.acdntMngNo, + label: `${inc.pollNm} (${inc.pollDate ? inc.pollDate.replace('T', ' ') : '-'})`, }))} /> diff --git a/frontend/src/tabs/hns/components/HNSView.tsx b/frontend/src/tabs/hns/components/HNSView.tsx index 4a49115..4fc86e4 100755 --- a/frontend/src/tabs/hns/components/HNSView.tsx +++ b/frontend/src/tabs/hns/components/HNSView.tsx @@ -265,6 +265,7 @@ export function HNSView() { const [leftCollapsed, setLeftCollapsed] = useState(false); const [rightCollapsed, setRightCollapsed] = useState(false); const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null); + const [flyToCoord, setFlyToCoord] = useState<{ lon: number; lat: number } | undefined>(undefined); const [isSelectingLocation, setIsSelectingLocation] = useState(false); const [isRunningPrediction, setIsRunningPrediction] = useState(false); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -904,6 +905,7 @@ export function HNSView() { onParamsChange={handleParamsChange} onReset={handleReset} loadedParams={loadedParams} + onFlyToCoord={(c) => setFlyToCoord({ lat: c.lat, lon: c.lon })} /> )} @@ -963,6 +965,8 @@ export function HNSView() { <> setFlyToCoord(undefined)} isSelectingLocation={isSelectingLocation} onMapClick={handleMapClick} oilTrajectory={[]} diff --git a/frontend/src/tabs/prediction/components/LeftPanel.tsx b/frontend/src/tabs/prediction/components/LeftPanel.tsx index 4edb0b6..4d0a1cb 100755 --- a/frontend/src/tabs/prediction/components/LeftPanel.tsx +++ b/frontend/src/tabs/prediction/components/LeftPanel.tsx @@ -112,6 +112,7 @@ export function LeftPanel({ onLayerColorChange, sensitiveResources = [], onImageAnalysisResult, + onFlyToCoord, validationErrors, }: LeftPanelProps) { const [expandedSections, setExpandedSections] = useState({ @@ -166,6 +167,7 @@ export function LeftPanel({ spillUnit={spillUnit} onSpillUnitChange={onSpillUnitChange} onImageAnalysisResult={onImageAnalysisResult} + onFlyToCoord={onFlyToCoord} validationErrors={validationErrors} /> diff --git a/frontend/src/tabs/prediction/components/OilSpillView.tsx b/frontend/src/tabs/prediction/components/OilSpillView.tsx index f276a0b..3909742 100755 --- a/frontend/src/tabs/prediction/components/OilSpillView.tsx +++ b/frontend/src/tabs/prediction/components/OilSpillView.tsx @@ -1208,6 +1208,9 @@ export function OilSpillView() { onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))} sensitiveResources={sensitiveResourceCategories} onImageAnalysisResult={handleImageAnalysisResult} + onFlyToCoord={(c: { lon: number; lat: number }) => + setFlyToCoord({ lat: c.lat, lon: c.lon }) + } validationErrors={validationErrors} /> diff --git a/frontend/src/tabs/prediction/components/PredictionInputSection.tsx b/frontend/src/tabs/prediction/components/PredictionInputSection.tsx index 68461ad..9366e18 100644 --- a/frontend/src/tabs/prediction/components/PredictionInputSection.tsx +++ b/frontend/src/tabs/prediction/components/PredictionInputSection.tsx @@ -1,8 +1,8 @@ import { useState, useRef, useEffect } from 'react'; import { ComboBox } from '@common/components/ui/ComboBox'; import type { PredictionModel } from './OilSpillView'; -import { analyzeImage } from '../services/predictionApi'; -import type { ImageAnalyzeResult } from '../services/predictionApi'; +import { analyzeImage, fetchGscAccidents } from '../services/predictionApi'; +import type { ImageAnalyzeResult, GscAccidentListItem } from '../services/predictionApi'; interface PredictionInputSectionProps { expanded: boolean; @@ -33,6 +33,7 @@ interface PredictionInputSectionProps { spillUnit: string; onSpillUnitChange: (unit: string) => void; onImageAnalysisResult?: (result: ImageAnalyzeResult) => void; + onFlyToCoord?: (coord: { lon: number; lat: number }) => void; validationErrors?: Set; } @@ -64,6 +65,7 @@ const PredictionInputSection = ({ spillUnit, onSpillUnitChange, onImageAnalysisResult, + onFlyToCoord, validationErrors, }: PredictionInputSectionProps) => { const [inputMode, setInputMode] = useState<'direct' | 'upload'>('direct'); @@ -71,8 +73,41 @@ const PredictionInputSection = ({ const [isAnalyzing, setIsAnalyzing] = useState(false); const [analyzeError, setAnalyzeError] = useState(null); const [analyzeResult, setAnalyzeResult] = useState(null); + const [gscAccidents, setGscAccidents] = useState([]); + const [selectedGscMngNo, setSelectedGscMngNo] = useState(''); const fileInputRef = useRef(null); + useEffect(() => { + let cancelled = false; + fetchGscAccidents() + .then((list) => { + if (!cancelled) setGscAccidents(list); + }) + .catch((err) => { + console.error('[prediction] GSC 사고 목록 조회 실패:', err); + }); + return () => { + cancelled = true; + }; + }, []); + + const handleGscAccidentSelect = (mngNo: string) => { + setSelectedGscMngNo(mngNo); + const item = gscAccidents.find((a) => a.acdntMngNo === mngNo); + if (!item) return; + onIncidentNameChange(item.pollNm); + if (item.pollDate) onAccidentTimeChange(item.pollDate); + if (item.lat != null && item.lon != null) { + onCoordChange({ lat: item.lat, lon: item.lon }); + onFlyToCoord?.({ lat: item.lat, lon: item.lon }); + } + }; + + const gscOptions = gscAccidents.map((a) => ({ + value: a.acdntMngNo, + label: `${a.pollNm} (${a.pollDate ? a.pollDate.replace('T', ' ') : '-'})`, + })); + const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0] ?? null; setUploadedFile(file); @@ -161,7 +196,13 @@ const PredictionInputSection = ({ : undefined } /> - + {/* Image Upload Mode */} {inputMode === 'upload' && ( diff --git a/frontend/src/tabs/prediction/components/leftPanelTypes.ts b/frontend/src/tabs/prediction/components/leftPanelTypes.ts index 4ade4ad..82933ae 100644 --- a/frontend/src/tabs/prediction/components/leftPanelTypes.ts +++ b/frontend/src/tabs/prediction/components/leftPanelTypes.ts @@ -60,6 +60,8 @@ export interface LeftPanelProps { sensitiveResources?: SensitiveResourceCategory[]; // 이미지 분석 결과 콜백 onImageAnalysisResult?: (result: ImageAnalyzeResult) => void; + // 사고 리스트 선택 시 지도 이동 콜백 + onFlyToCoord?: (coord: { lon: number; lat: number }) => void; // 유효성 검증 에러 필드 validationErrors?: Set; } diff --git a/frontend/src/tabs/prediction/services/predictionApi.ts b/frontend/src/tabs/prediction/services/predictionApi.ts index 64c446f..e2783eb 100644 --- a/frontend/src/tabs/prediction/services/predictionApi.ts +++ b/frontend/src/tabs/prediction/services/predictionApi.ts @@ -321,3 +321,20 @@ export const analyzeImage = async (file: File, acdntNm?: string): Promise => { + const response = await api.get('/gsc/accidents'); + return response.data; +}; diff --git a/frontend/src/tabs/rescue/components/RescueView.tsx b/frontend/src/tabs/rescue/components/RescueView.tsx index 59d27d7..1244f38 100755 --- a/frontend/src/tabs/rescue/components/RescueView.tsx +++ b/frontend/src/tabs/rescue/components/RescueView.tsx @@ -5,8 +5,8 @@ import { RescueTheoryView } from './RescueTheoryView'; import { RescueScenarioView } from './RescueScenarioView'; import { fetchRescueOps } from '../services/rescueApi'; import type { RescueOpsItem } from '../services/rescueApi'; -import { fetchIncidentsRaw } from '@tabs/incidents/services/incidentsApi'; -import type { IncidentListItem } from '@tabs/incidents/services/incidentsApi'; +import { fetchGscAccidents } from '@tabs/prediction/services/predictionApi'; +import type { GscAccidentListItem } from '@tabs/prediction/services/predictionApi'; /* ─── Types ─── */ type AccidentType = @@ -230,9 +230,9 @@ function LeftPanel({ }: { activeType: AccidentType; onTypeChange: (t: AccidentType) => void; - incidents: IncidentListItem[]; - selectedAcdnt: IncidentListItem | null; - onSelectAcdnt: (item: IncidentListItem | null) => void; + incidents: GscAccidentListItem[]; + selectedAcdnt: GscAccidentListItem | null; + onSelectAcdnt: (item: GscAccidentListItem | null) => void; }) { const [acdntName, setAcdntName] = useState(''); const [acdntDate, setAcdntDate] = useState(''); @@ -242,18 +242,25 @@ function LeftPanel({ const [showList, setShowList] = useState(false); // 사고 선택 시 필드 자동 채움 - const handlePickIncident = (item: IncidentListItem) => { + const handlePickIncident = (item: GscAccidentListItem) => { onSelectAcdnt(item); - setAcdntName(item.acdntNm); - const dt = new Date(item.occrnDtm); - setAcdntDate( - `${dt.getFullYear()}. ${String(dt.getMonth() + 1).padStart(2, '0')}. ${String(dt.getDate()).padStart(2, '0')}.`, - ); - setAcdntTime( - `${dt.getHours() >= 12 ? '오후' : '오전'} ${String(dt.getHours() % 12 || 12).padStart(2, '0')}:${String(dt.getMinutes()).padStart(2, '0')}`, - ); - setAcdntLat(String(item.lat)); - setAcdntLon(String(item.lng)); + setAcdntName(item.pollNm); + if (item.pollDate) { + const [d, t] = item.pollDate.split('T'); + if (d) { + const [y, m, day] = d.split('-'); + setAcdntDate(`${y}. ${m}. ${day}.`); + } + if (t) { + const [hhStr, mmStr] = t.split(':'); + const hh = parseInt(hhStr, 10); + const ampm = hh >= 12 ? '오후' : '오전'; + const hh12 = String(hh % 12 || 12).padStart(2, '0'); + setAcdntTime(`${ampm} ${hh12}:${mmStr}`); + } + } + if (item.lat != null) setAcdntLat(String(item.lat)); + if (item.lon != null) setAcdntLon(String(item.lon)); setShowList(false); }; @@ -283,7 +290,7 @@ function LeftPanel({ className="w-full px-2 py-1.5 text-caption bg-bg-card border border-stroke rounded font-korean text-left cursor-pointer hover:border-[var(--stroke-light)] flex items-center justify-between" > - {selectedAcdnt ? selectedAcdnt.acdntCd : '또는 사고 리스트에서 선택'} + {selectedAcdnt ? selectedAcdnt.pollNm : '또는 사고 리스트에서 선택'} {showList ? '▲' : '▼'} @@ -296,13 +303,13 @@ function LeftPanel({ )} {incidents.map((item) => ( ))} @@ -1523,13 +1530,14 @@ export function RescueView() { const { activeSubTab } = useSubMenu('rescue'); const [activeType, setActiveType] = useState('collision'); const [activeAnalysis, setActiveAnalysis] = useState('rescue'); - const [incidents, setIncidents] = useState([]); - const [selectedAcdnt, setSelectedAcdnt] = useState(null); + const [incidents, setIncidents] = useState([]); + const [selectedAcdnt, setSelectedAcdnt] = useState(null); const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null); + const [flyToCoord, setFlyToCoord] = useState<{ lon: number; lat: number } | undefined>(undefined); const [isSelectingLocation, setIsSelectingLocation] = useState(false); useEffect(() => { - fetchIncidentsRaw() + fetchGscAccidents() .then((items) => setIncidents(items)) .catch(() => setIncidents([])); }, []); @@ -1540,23 +1548,13 @@ export function RescueView() { setIsSelectingLocation(false); }, []); - // 사고 선택 시 사고유형 자동 매핑 + // 사고 선택 시 좌표 자동 반영 + 지도 이동 const handleSelectAcdnt = useCallback( - (item: IncidentListItem | null) => { + (item: GscAccidentListItem | null) => { setSelectedAcdnt(item); - if (item) { - const typeMap: Record = { - collision: 'collision', - grounding: 'grounding', - turning: 'turning', - capsizing: 'capsizing', - sharpTurn: 'sharpTurn', - flooding: 'flooding', - sinking: 'sinking', - }; - const mapped = typeMap[item.acdntTpCd]; - if (mapped) setActiveType(mapped); - setIncidentCoord({ lon: item.lng, lat: item.lat }); + if (item && item.lat != null && item.lon != null) { + setIncidentCoord({ lon: item.lon, lat: item.lat }); + setFlyToCoord({ lon: item.lon, lat: item.lat }); } }, [], @@ -1595,6 +1593,8 @@ export function RescueView() {
setFlyToCoord(undefined)} isSelectingLocation={isSelectingLocation} onMapClick={handleMapClick} oilTrajectory={[]} From 679649ab8cf1802988aa7245973778fea7ec1c44 Mon Sep 17 00:00:00 2001 From: "jeonghyo.k" Date: Wed, 15 Apr 2026 08:16:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 8dd07de..1095133 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,9 @@ ## [Unreleased] +### 추가 +- 확산예측·HNS 대기확산·긴급구난: GSC 외부 사고 목록 API 연동 및 셀렉트박스 자동 채움 (사고명·발생시각·위경도 자동 입력 + 지도 이동) + ### 변경 - MapView 컴포넌트 분리 및 전체 탭 디자인 시스템 토큰 적용 From 6b19d34e5b66a547898c9d45892aa48c38617b50 Mon Sep 17 00:00:00 2001 From: "jeonghyo.k" Date: Wed, 15 Apr 2026 08:22:57 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=ED=8C=80=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=84=A4=EC=A0=95=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - settings.json Bash 권한 항목 세분화 - workflow-version.json 적용일 갱신 (2026-04-14) --- .claude/settings.json | 25 ++++++++++++++++++++++++- .claude/workflow-version.json | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 441dc35..c8c5d77 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,7 +5,30 @@ }, "permissions": { "allow": [ - "Bash(*)" + "Bash(*)", + "Bash(npm run *)", + "Bash(npm install *)", + "Bash(npm test *)", + "Bash(npx *)", + "Bash(node *)", + "Bash(git status)", + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git branch *)", + "Bash(git checkout *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(git pull *)", + "Bash(git fetch *)", + "Bash(git merge *)", + "Bash(git stash *)", + "Bash(git remote *)", + "Bash(git config *)", + "Bash(git rev-parse *)", + "Bash(git show *)", + "Bash(git tag *)", + "Bash(curl -s *)", + "Bash(fnm *)" ], "deny": [ "Bash(git push --force*)", diff --git a/.claude/workflow-version.json b/.claude/workflow-version.json index 03381a9..ffa771a 100644 --- a/.claude/workflow-version.json +++ b/.claude/workflow-version.json @@ -1,6 +1,6 @@ { "applied_global_version": "1.6.1", - "applied_date": "2026-03-31", + "applied_date": "2026-04-14", "project_type": "react-ts", "gitea_url": "https://gitea.gc-si.dev", "custom_pre_commit": true