Files
sam-manage/resources/views/juil/pmis-corrective-action.blade.php
김보곤 2102f4a398 feat: [pmis] PMIS 자료실/안전관리/품질관리 기능 추가 및 개선
- 자료실 하위 3개 메뉴: 자료보관함, 매뉴얼, 공지사항
- 자료보관함: 폴더 트리 + 파일 업로드/다운로드/삭제
- 매뉴얼/공지사항: 게시판형 CRUD + 첨부파일
- 안전관리: 안전보건교육, TBM현황, 위험성평가, 재해예방조치
- 품질관리: 시정조치 UI 페이지
- 대시보드: 슈퍼관리자 전용 레거시 사이트 참고 카드
- 작업일보/출면일보 오류 수정 및 기능 개선
- 설비 사진 업로드, 근로계약서 종료일 수정
2026-03-12 21:11:21 +09:00

222 lines
16 KiB
PHP

@extends('layouts.app')
@section('title', '시정조치 - 건설PMIS')
@section('content')
<div id="root"></div>
@endsection
@push('scripts')
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" rel="stylesheet" />
@include('partials.react-cdn')
<script type="text/babel">
@verbatim
const { useState, useMemo } = React;
/* ═══════ 사이드바 ═══════ */
const PMIS_MENUS=[
{icon:'ri-building-2-line',label:'BIM 관리',id:'bim',children:[{label:'BIM 뷰어',id:'bim-viewer',url:'/juil/construction-pmis/bim-viewer'}]},
{icon:'ri-line-chart-line',label:'시공관리',id:'construction',children:[
{label:'인원관리',id:'workforce',url:'/juil/construction-pmis/workforce'},
{label:'장비관리',id:'equipment',url:'/juil/construction-pmis/equipment'},
{label:'자재관리',id:'materials',url:'/juil/construction-pmis/materials'},
{label:'공사량관리',id:'work-volume',url:'/juil/construction-pmis/work-volume'},
{label:'출면일보',id:'daily-attendance',url:'/juil/construction-pmis/daily-attendance'},
{label:'작업일보',id:'daily-report',url:'/juil/construction-pmis/daily-report'},
]},
{icon:'ri-file-list-3-line',label:'품질관리',id:'quality',children:[
{label:'시정조치',id:'corrective-action',url:'/juil/construction-pmis/corrective-action'},
]},
{icon:'ri-shield-check-line',label:'안전관리',id:'safety',children:[
{label:'안전보건교육',id:'safety-education',url:'/juil/construction-pmis/safety-education'},
{label:'TBM현장',id:'tbm',url:'/juil/construction-pmis/tbm'},
{label:'위험성 평가',id:'risk-assessment',url:'/juil/construction-pmis/risk-assessment'},
{label:'재해예방조치',id:'disaster-prevention',url:'/juil/construction-pmis/disaster-prevention'},
]},
{icon:'ri-folder-line',label:'자료실',id:'archive',children:[
{label:'자료보관함',id:'archive-files',url:'/juil/construction-pmis/archive-files'},
{label:'매뉴얼',id:'archive-manual',url:'/juil/construction-pmis/archive-manual'},
{label:'공지사항',id:'archive-notice',url:'/juil/construction-pmis/archive-notice'},
]},
];
function PmisSidebar({activePage}){
const[profile,setProfile]=useState(null);
const[expanded,setExpanded]=useState(()=>{for(const m of PMIS_MENUS){if(m.children?.some(c=>c.id===activePage))return m.id}return null});
React.useEffect(()=>{fetch('/juil/construction-pmis/profile',{headers:{Accept:'application/json'}}).then(r=>r.json()).then(d=>setProfile(d.worker)).catch(()=>{})},[]);
return(
<div className="bg-white border-r border-gray-200 shadow-sm flex flex-col shrink-0" style={{width:200}}>
<a href="/juil/construction-pmis" className="flex items-center gap-2 px-4 py-3 text-sm text-blue-600 hover:bg-blue-50 border-b border-gray-100 transition"><i className="ri-arrow-left-s-line text-lg"></i> PMIS 대시보드</a>
<div className="p-3 border-b border-gray-100 text-center">
<div className="w-12 h-12 mx-auto mb-1 rounded-full bg-gray-100 border-2 border-gray-200 flex items-center justify-center">
{profile?.profile_photo_path?<img src={profile.profile_photo_path} className="w-full h-full rounded-full object-cover"/>:<i className="ri-user-3-line text-xl text-gray-300"></i>}
</div>
<div className="text-sm font-bold text-gray-800">{profile?.name||'...'}</div>
<div className="text-xs text-gray-500 mt-0.5">{profile?.department||''}</div>
</div>
<div className="flex-1 overflow-auto py-1">
{PMIS_MENUS.map(m=>(
<div key={m.id}>
<div onClick={()=>setExpanded(expanded===m.id?null:m.id)} className={`flex items-center gap-2 px-4 py-2.5 text-sm cursor-pointer transition ${expanded===m.id?'bg-blue-50 text-blue-700 font-semibold':'text-gray-600 hover:bg-gray-50'}`}>
<i className={`${m.icon} text-base`}></i>{m.label}
{m.children&&<i className={`ml-auto ${expanded===m.id?'ri-arrow-down-s-line':'ri-arrow-right-s-line'} text-gray-400 text-xs`}></i>}
</div>
{expanded===m.id&&m.children?.map(c=>(<a key={c.id} href={c.url} className={`block pl-10 pr-4 py-2 text-sm transition ${c.id===activePage?'bg-blue-100 text-blue-800 font-semibold border-l-2 border-blue-600':'text-gray-500 hover:text-blue-600 hover:bg-gray-50'}`}>{c.label}</a>))}
</div>))}
</div>
</div>);
}
/* ═══════ 샘플 데이터 ═══════ */
const SAMPLE_DATA = [
{ id:1, photo:'/images/placeholder.png', content:'3층 복도 천장 균열 발생 - 마감재 들뜸 확인', requester:'김정훈', status:'조치완료', requestDate:'2026-02-15', actionDate:'2026-02-20', dDay:0, company:'(주)주일기업', erpDate:'2026-02-21', location:'A동 3층 복도' },
{ id:2, photo:'', content:'지하1층 방수층 손상 - 누수 흔적 발견', requester:'이성태', status:'조치중', requestDate:'2026-03-01', actionDate:'', dDay:11, company:'KCC건설', erpDate:'', location:'B동 지하1층' },
{ id:3, photo:'', content:'외벽 타일 탈락 위험 구간 발견', requester:'안성현', status:'미조치', requestDate:'2026-03-05', actionDate:'', dDay:7, company:'', erpDate:'', location:'A동 외벽 남측' },
{ id:4, photo:'', content:'소방 배관 연결부 이완 - 재시공 필요', requester:'심준수', status:'조치중', requestDate:'2026-03-08', actionDate:'', dDay:4, company:'(주)주일기업', erpDate:'', location:'B동 2층 기계실' },
{ id:5, photo:'', content:'방화셔터 작동 불량 - 센서 점검 요청', requester:'정영진', status:'미조치', requestDate:'2026-03-10', actionDate:'', dDay:2, company:'(주)주일기업', erpDate:'', location:'A동 1층 로비' },
];
const STATUS_COLORS = { '조치완료':'bg-green-100 text-green-700', '조치중':'bg-yellow-100 text-yellow-700', '미조치':'bg-red-100 text-red-700' };
/* ═══════ 메인 ═══════ */
function App(){
const now = new Date();
const pad = (n) => String(n).padStart(2,'0');
const todayStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}`;
const monthAgo = new Date(now); monthAgo.setMonth(monthAgo.getMonth()-1);
const monthAgoStr = `${monthAgo.getFullYear()}-${pad(monthAgo.getMonth()+1)}-${pad(monthAgo.getDate())}`;
const [filter, setFilter] = useState('조치완료 제외');
const [dateType, setDateType] = useState('처리완료일');
const [dateFrom, setDateFrom] = useState(monthAgoStr);
const [dateTo, setDateTo] = useState(todayStr);
const [delayed, setDelayed] = useState(false);
const [searchType, setSearchType] = useState('요청내용');
const [searchText, setSearchText] = useState('');
const [checked, setChecked] = useState({});
const filtered = useMemo(() => {
let list = [...SAMPLE_DATA];
if (filter === '조치완료 제외') list = list.filter(d => d.status !== '조치완료');
if (delayed) list = list.filter(d => d.dDay > 0 && d.status !== '조치완료');
if (searchText) {
const q = searchText.toLowerCase();
list = list.filter(d => {
if (searchType === '요청내용') return d.content.toLowerCase().includes(q);
if (searchType === '요청자') return d.requester.includes(q);
if (searchType === '위치') return d.location.toLowerCase().includes(q);
return true;
});
}
return list;
}, [filter, delayed, searchText, searchType]);
const allChecked = filtered.length > 0 && filtered.every(d => checked[d.id]);
return(
<div className="flex bg-gray-100" style={{height:'calc(100vh - 56px)'}}>
<PmisSidebar activePage="corrective-action"/>
<div className="flex-1 flex flex-col overflow-hidden">
{/* 헤더 */}
<div className="bg-white border-b border-gray-200 px-6 py-4">
<div className="flex items-center gap-2 text-xs text-gray-400 mb-2">
<i className="ri-home-4-line"></i><span>Home</span> &gt; <span>품질관리</span> &gt; <span className="text-gray-600">시정조치</span>
</div>
<div className="flex items-center justify-between">
<h1 className="text-lg font-bold text-gray-800">시정조치</h1>
<div className="flex items-center gap-2">
<button className="border border-gray-300 px-4 py-1.5 rounded text-sm hover:bg-gray-50">양식출력</button>
<button className="bg-gray-600 text-white px-4 py-1.5 rounded text-sm hover:bg-gray-700">일괄다운로드</button>
</div>
</div>
</div>
{/* 필터 바 */}
<div className="bg-white border-b border-gray-200 px-6 py-3">
<div className="flex items-center gap-3 flex-wrap">
<select value={filter} onChange={e=>setFilter(e.target.value)} className="border border-gray-300 rounded px-2 py-1.5 text-sm bg-white">
<option>조치완료 제외</option><option>전체</option><option>미조치</option><option>조치중</option><option>조치완료</option>
</select>
<select value={dateType} onChange={e=>setDateType(e.target.value)} className="border border-gray-300 rounded px-2 py-1.5 text-sm bg-white">
<option>처리완료일</option><option>요청일</option><option>조치일</option>
</select>
<input type="date" value={dateFrom} onChange={e=>setDateFrom(e.target.value)} className="border border-gray-300 rounded px-2 py-1.5 text-sm"/>
<span className="text-gray-400">~</span>
<input type="date" value={dateTo} onChange={e=>setDateTo(e.target.value)} className="border border-gray-300 rounded px-2 py-1.5 text-sm"/>
<label className="flex items-center gap-1.5 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" checked={delayed} onChange={e=>setDelayed(e.target.checked)}/> 지연여부
</label>
<select value={searchType} onChange={e=>setSearchType(e.target.value)} className="border border-gray-300 rounded px-2 py-1.5 text-sm bg-white">
<option>요청내용</option><option>요청자</option><option>위치</option>
</select>
<input type="text" value={searchText} onChange={e=>setSearchText(e.target.value)} placeholder="검색어" className="border border-gray-300 rounded px-2 py-1.5 text-sm" style={{width:140}}/>
<button className="bg-blue-600 text-white px-5 py-1.5 rounded text-sm font-semibold hover:bg-blue-700">검색</button>
</div>
</div>
{/* 테이블 */}
<div className="flex-1 overflow-auto p-6">
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:35}}>
<input type="checkbox" checked={allChecked} onChange={()=>{const c={};if(!allChecked)filtered.forEach(d=>c[d.id]=true);setChecked(c)}}/>
</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:45}}>No</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:80}}>요청사진</th>
<th className="px-3 py-2.5 text-left font-semibold text-gray-600">요청내용</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:70}}>요청자</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:80}}>처리상태</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:95}}>요청일</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:95}}>조치일</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:55}}>D-Day</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:100}}>조치업체</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:95}}>ERP<br/>등록일</th>
<th className="px-3 py-2.5 text-center font-semibold text-gray-600" style={{width:120}}>위치</th>
</tr>
</thead>
<tbody>
{filtered.map((d,i) => (
<tr key={d.id} className="border-b border-gray-100 hover:bg-blue-50/30 transition">
<td className="px-3 py-2.5 text-center"><input type="checkbox" checked={!!checked[d.id]} onChange={()=>setChecked(p=>({...p,[d.id]:!p[d.id]}))}/></td>
<td className="px-3 py-2.5 text-center text-gray-500">{i+1}</td>
<td className="px-3 py-2.5 text-center">
<div className="w-10 h-10 mx-auto bg-gray-100 rounded border border-gray-200 flex items-center justify-center">
<i className="ri-image-line text-gray-300"></i>
</div>
</td>
<td className="px-3 py-2.5 text-gray-700">{d.content}</td>
<td className="px-3 py-2.5 text-center text-gray-700">{d.requester}</td>
<td className="px-3 py-2.5 text-center">
<span className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${STATUS_COLORS[d.status]||'bg-gray-100 text-gray-600'}`}>{d.status}</span>
</td>
<td className="px-3 py-2.5 text-center text-gray-600">{d.requestDate}</td>
<td className="px-3 py-2.5 text-center text-gray-600">{d.actionDate||'-'}</td>
<td className="px-3 py-2.5 text-center">
{d.status!=='조치완료'&&d.dDay>0?<span className="text-red-500 font-semibold">D+{d.dDay}</span>:<span className="text-gray-400">-</span>}
</td>
<td className="px-3 py-2.5 text-center text-gray-600">{d.company||'-'}</td>
<td className="px-3 py-2.5 text-center text-gray-600">{d.erpDate||'-'}</td>
<td className="px-3 py-2.5 text-center text-gray-600">{d.location}</td>
</tr>
))}
{filtered.length===0&&(
<tr><td colSpan={12} className="text-center py-16 text-gray-400">조회된 데이터가 없습니다.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>);
}
ReactDOM.render(<App/>, document.getElementById('root'));
@endverbatim
</script>
@endpush