IT Portal(IT 정보화 포탈)은 정보화 예산, 사업, 인력을 관리하는 사내 포털 시스템입니다.
약 3,000명의 임직원이 정보화사업 신청, 예산 현황 조회, 전자결재 등의 기능을 이용합니다.
주요 모듈:
- 정보화사업 관리(프로젝트 등록/조회/결재)
- 예산 관리(전산예산/전산업무비/예산현황/예산 작업)
- 요구사항 정의서 및 사전협의(문서 검토/코멘트)
- 전자결재 프로세스
- 정보화실무협의회(타당성검토/위원선정/평가/결과)
- 시스템 관리(사용자/권한/조직/코드 관리)
- PrimeVue Aura 테마 기반 UI
- Tailwind CSS로 추가 스타일링 및 커스텀 유틸리티(
kdb-tag-*등) app/assets/css/main.css에 전역 색상 정의 및 다크모드 스타일StyledDataTable.vue로 모든 DataTable 통일(파란 헤더, gridlines, resizable)
| 구분 | 기술 | 버전 | 용도 |
|---|---|---|---|
| 프레임워크 | Nuxt 4 | v4.0.0 | Composition API + <script setup>, CSR(SPA) 모드 |
| UI 라이브러리 | PrimeVue | v4.5.4 | Aura 테마, 한국어 로케일, DataTable/Dialog/Dropdown 등 |
| 스타일링 | Tailwind CSS | v3.4.17 | 유틸리티 기반 스타일, main.css에 커스텀 색상(kdb-tag-*) |
| 상태관리 | Pinia | v3.0.4 | 인증 상태(stores/auth.ts), 사전협의 세션(stores/review.ts) |
| 리치 텍스트 | Tiptap | v3.22.2 | 요구사항 정의서/가이드 문서 에디터 (20+ 확장) |
| 다이어그램 | Excalidraw | v0.17.6 | Tiptap 커스텀 노드로 통합, 다이어그램 삽입/재편집 |
| PDF 생성 | pdfmake | v0.3.3 | 정보화사업 보고서 PDF 생성, 한글 폰트 지원 |
| Excel 내보내기 | ExcelJS | v4.2.0 | 예산/비용 데이터 Excel 내보내기 |
| 한글(HWPX) | JSZip | v3.10.1 | HTML→HWPX 변환 (클라이언트 전용 변환, utils/hwpx.ts) |
| XSS 방어 | isomorphic-dompurify | v3.0.0 | v-html 사용 시 필수 새니타이징 |
| 이미지 캡처 | html2canvas | v1.4.1 | 화면 스크린샷 생성 |
| 수식 입력 | mathlive | v0.109.0 | 인라인/블록 LaTeX 수식 노드 |
| 코드 하이라이팅 | lowlight | v3.3.0 | Tiptap 코드 블록 문법 강조 |
| 차트 | Chart.js | v4.5.1 | 대시보드 KPI, 월별 통계 시각화 |
| AI 연동 | Gemini API | - | GeminiChat.vue 플로팅 채팅 패널 |
소스 루트: app/ (Nuxt 4 convention)
app/
├── components/ # 재사용 컴포넌트 (63개 .vue 파일)
│ ├── AppHeader.vue # 상단 헤더 (메가메뉴, 탭바, 사용자 메뉴, 테마 토글)
│ ├── AppSidebar.vue # 좌측 사이드바 (동적 메뉴, 결재 대기 배지)
│ ├── PageHeader.vue # 페이지 제목/경로 표시
│ ├── GlobalSearchBar.vue # 전역 검색바
│ ├── TiptapEditor.vue # Tiptap 통합 리치 텍스트 에디터 (20+ 확장)
│ ├── TiptapToolbar.vue # Tiptap 포맷팅 툴바
│ ├── TiptapTableFloatingToolbar.vue # Tiptap 표 편집 플로팅 툴바
│ ├── GeminiChat.vue # AI 채팅 플로팅 패널 (Gemini API 연동)
│ ├── ExcalidrawWrapper.vue # Excalidraw React 라이브러리 브리지
│ ├── ExcalidrawNodeView.vue # Tiptap 다이어그램 노드 뷰
│ ├── ResizableImageNodeView.vue # Tiptap 크기 조절 이미지 노드
│ ├── AttachmentNodeView.vue # Tiptap 첨부파일 노드
│ ├── InlineMathNodeView.vue # Tiptap 인라인 LaTeX 수식 노드
│ ├── BlockMathNodeView.vue # Tiptap 블록 LaTeX 수식 노드
│ ├── ApplicationViewerDialog.vue # 신청서 뷰어 다이얼로그 (결재용)
│ ├── AppDialogHeader.vue # 다이얼로그 헤더 (공통)
│ ├── AppDialogFooter.vue # 다이얼로그 푸터 (공통)
│ ├── TableCard.vue # 테이블 카드 래퍼
│ ├── approval/ # 전자결재 컴포넌트 (2개)
│ │ ├── ApprovalTimeline.vue # 결재 프로세스 타임라인
│ │ └── ApplicationViewerDialog.vue
│ ├── budget/ # 예산 컴포넌트 (5개)
│ │ ├── BudgetSummaryCards.vue # 예산 현황 4개 카드
│ │ ├── BudgetTableActions.vue # 목록 행동 컴포넌트
│ │ ├── BudgetSummaryResultTable.vue # 예산 편성 결과 테이블
│ │ ├── BudgetProjectSummaryTable.vue # 예산 사업 요약 테이블
│ │ └── BudgetTargetTable.vue # 예산 목표 테이블
│ ├── cost/ # 전산업무비 컴포넌트 (3개)
│ │ ├── CostFormTableSection.vue # 비용 폼 섹션
│ │ ├── TerminalTableSection.vue # 단말기 테이블 섹션
│ │ └── TerminalFormDialog.vue # 단말기 폼 다이얼로그
│ ├── council/ # 정보화실무협의회 컴포넌트 (15개)
│ │ ├── CouncilStatusBadge.vue # 협의회 상태 배지
│ │ ├── committee/ # 위원 선정 (CommitteeSelector, CommitteeList)
│ │ ├── evaluation/ # 평가 (EvaluationForm, EvalSummaryPanel)
│ │ ├── feasibility/ # 타당성검토 (4개)
│ │ ├── schedule/ # 일정 (ScheduleInput, ScheduleStatus)
│ │ ├── qna/ # 사전질의 (CouncilQna)
│ │ ├── notice/ # 공지 (CouncilNotice)
│ │ └── result/ # 결과 (ResultForm, ResultReview)
│ ├── plan/ # 정보기술부문 계획 컴포넌트 (3개)
│ │ ├── PlanCapitalBudgetCard.vue
│ │ ├── PlanExpenseCostCard.vue
│ │ └── PlanBudgetAllocationCard.vue
│ ├── projects/ # 정보화사업 컴포넌트 (3개)
│ │ ├── ProjectOverviewSection.vue # 프로젝트 개요
│ │ ├── ProjectProgressSection.vue # 진행 상황
│ │ └── ResourceTableSection.vue # 소요자원 테이블
│ ├── review/ # 사전협의/문서 검토 컴포넌트 (5개)
│ │ ├── ReviewEditor.vue # 협의 코멘트 에디터
│ │ ├── ReviewToolbar.vue # 협의 제목/상태 툴바
│ │ ├── ReviewMessenger.vue # 협의 코멘트 메시지
│ │ ├── ReviewCommentPopover.vue # 인라인 코멘트 팝오버
│ │ └── ReviewVersionHistory.vue # 버전 이력 뷰
│ ├── icons/ # 커스텀 아이콘 (1개)
│ │ └── IconCrown.vue
│ ├── extensions/ # Tiptap 커스텀 확장
│ └── common/ # 공용 컴포넌트
│ ├── StyledDataTable.vue # PrimeVue DataTable 래퍼 (파란 헤더, gridlines)
│ ├── EmployeeSearchDialog.vue # 직원 검색 다이얼로그 (조직 트리 + 사용자 목록)
│ ├── EmployeeInfoDialog.vue # 직원 정보 상세 다이얼로그
│ ├── VersionHistoryDialog.vue # 버전 이력 다이얼로그
│ ├── TableSearchInput.vue # 테이블 검색 입력
│ ├── InlineEditCell.vue # 테이블 인라인 편집 셀
├── composables/ # 비즈니스 로직 및 API 래퍼 (41개 .ts 파일)
│ │
│ ├── 인증 및 API 래퍼
│ │ ├── useAuth.ts # 인증 상태 접근 (Pinia 래퍼, $apiFetch 제공)
│ │ ├── useApiFetch.ts # GET 요청 래퍼 (useFetch 기반, httpOnly 쿠키 자동 전송)
│ │ ├── useAdminApi.ts # 관리자 CRUD API (공통코드/사용자/역할/조직/파일/토큰/이력)
│ │ └── useAdminTableEdit.ts # 관리자 테이블 인라인 편집
│ │
│ ├── 정보화사업 및 프로젝트
│ │ ├── useProjects.ts # 정보화사업 CRUD (/api/projects)
│ │ ├── useProjectOptions.ts # 프로젝트 옵션 (연도/분류/상태 코드)
│ │
│ ├── 예산 관리
│ │ ├── useBudgetPeriod.ts # 예산 기준연도/기간 미들웨어 보조
│ │ ├── useBudgetStatus.ts # 예산현황 3탭(정보화사업/전산업무비/경상사업) 조회
│ │ ├── useBudgetStatusCostTab.ts # 예산현황 전산업무비 탭 상세
│ │ ├── useBudgetAllocationSummary.ts # 예산 할당 요약
│ │
│ ├── 전산업무비
│ │ ├── useCost.ts # 전산업무비 CRUD (/api/costs)
│ │ ├── useCostListPage.ts # 전산업무비 목록 페이지 상태
│ │ ├── useCostDuplicateCheck.ts # 비용 중복 검사
│ │ └── usePrevYearCostPicker.ts # 전년도 비용 선택기
│ │
│ ├── 요구사항 정의서 및 사전협의
│ │ ├── useDocuments.ts # 요구사항 정의서 CRUD (/api/documents)
│ │ ├── useDocumentDashboard.ts # 사전협의 대시보드 (KPI + 월별 통계 + 배지 카운트)
│ │ ├── useReview.ts # 사전협의 세션 관리 (Pinia 스토어 래퍼)
│ │ └── useReviewCommentApi.ts # 사전협의 코멘트 서버 동기화
│ │
│ ├── 정보기술부문 계획
│ │ └── usePlan.ts # 정보기술부문 계획 CRUD (/api/plans)
│ │
│ ├── 정보화실무협의회
│ │ ├── useCouncil.ts # 협의회 CRUD 및 상태 전이 (/api/council)
│ │ └── useCouncilCodes.ts # 협의회 상태/심의유형 코드 조회
│ │
│ ├── 전자결재
│ │ ├── useApprovals.ts # 전자결재 CRUD (/api/applications)
│ │ ├── useApprovalDashboard.ts # 전자결재 대시보드 (KPI + 월별 통계 + 배지)
│ │ └── usePendingApprovalCount.ts # 결재 대기 배지 카운트
│ │
│ ├── 공통 데이터 및 조직
│ │ ├── useCodeOptions.ts # 공통코드 옵션 조회 (/api/ccodem)
│ │ ├── useOrganization.ts # 조직도 및 사용자 조회
│ │ ├── useEmployeeSearch.ts # 직원 검색 (AutoComplete + 다이얼로그)
│ │ ├── useCurrencyRates.ts # 환율 조회 및 원화 환산
│ │
│ ├── 파일 및 내보내기
│ │ ├── useFiles.ts # 첨부파일 업로드/다운로드/미리보기
│ │ ├── useHwpxExport.ts # HWPX(한글) 파일 내보내기
│ │ └── usePdfReport.ts # PDF 보고서 생성 (정보화사업)
│ │
│ ├── Tiptap 에디터 관련
│ │ ├── useExcalidrawDialog.ts # Excalidraw 편집 다이얼로그 상태
│ │ ├── useExcalidrawAttachment.ts # Excalidraw 첨부파일 임시 ID 추적
│ │ ├── useTiptapImageInsertion.ts # Tiptap 이미지 삽입 및 업로드
│ │ └── useTiptapTableTools.ts # Tiptap 표 편집 유틸리티
│ │
│ ├── UI/UX 유틸리티
│ │ ├── useGlobalSearch.ts # 전역 검색 상태 및 API
│ │ ├── useTabs.ts # 멀티탭 네비게이션 관리
│ │ ├── useTableCellSelection.ts # 엑셀 스타일 셀 선택/복사
│ │ ├── useTableColumnResize.ts # 테이블 열 너비 조정
│ │ ├── useDateRangeValidation.ts # 날짜 범위 유효성 검사
│ └── useGuideDocuments.ts # 가이드 문서 CRUD
├── layouts/ # 페이지 레이아웃 (3개)
│ ├── default.vue # 기본 레이아웃 (AppHeader + AppSidebar + 콘텐츠)
│ ├── admin.vue # 관리자 전용 레이아웃 (default와 동일, /admin 경로용)
│ └── login.vue # 로그인 페이지 전용 레이아웃
│
├── middleware/ # 라우트 가드 및 인증 미들웨어 (3개)
│ ├── auth.global.ts # 전역 인증 미들웨어 (미인증 사용자 /login 리다이렉트)
│ ├── admin.ts # 관리자 접근 제어 (ROLE.ADMIN 검증)
│ └── budget-period.ts # 예산 기준기간 라우트 가드
│
├── pages/ # 파일 기반 라우팅 (46개 .vue 파일)
│ ├── index.vue # 루트 (/ → /info 리다이렉트)
│ ├── login.vue # 로그인 페이지
│ │
│ ├── admin/ # 시스템 관리 (ROLE.ADMIN 전용, 10개 페이지)
│ │ ├── index.vue # 관리자 대시보드
│ │ ├── codes.vue # 공통코드 관리
│ │ ├── users.vue # 사용자 관리
│ │ ├── roles.vue # 역할 관리
│ │ ├── grades.vue # 자격등급 관리
│ │ ├── organizations.vue # 조직 관리
│ │ ├── files.vue # 파일 관리
│ │ ├── tokens.vue # 토큰 관리
│ │ ├── audit-logs.vue # 감사로그 조회
│ │ └── health.vue # 시스템 상태
│ │
│ ├── approval/ # 전자결재 (2개)
│ │ ├── index.vue # 전자결재 목록 + 대시보드 (KPI 카드)
│ │ └── [id].vue # 결재 상세 뷰 + 처리 폼
│ │
│ ├── audit/ # IT 자체감사 (1개)
│ │ └── index.vue # 감사 대시보드
│ │
│ ├── budget/ # 예산 관리 (5개)
│ │ ├── index.vue # 예산 작성 유형 선택 페이지
│ │ ├── list.vue # 예산 통합 목록 (전체/정보화사업/전산업무비/경상사업 탭)
│ │ ├── status.vue # 예산현황 (예산액/집행율 조회, 3탭)
│ │ ├── work.vue # 예산 작업 (편성률 일괄 적용)
│ │ └── detail-[type]-[id].vue # 예산 상세 및 편성
│ │
│ ├── diagnosis/ # 사전진단 (1개)
│ │ └── index.vue # 사전진단 설문
│ │
│ ├── guide/ # 가이드 문서 (3개)
│ │ ├── index.vue # 가이드 문서 목록
│ │ ├── [id].vue # 가이드 문서 상세 (Tiptap 뷰)
│ │ └── form.vue # 가이드 문서 등록/수정 (Tiptap 에디터)
│ │
│ ├── info/ # 주요 정보 관리
│ │ ├── index.vue # 대시보드 (홈)
│ │ │
│ │ ├── cost/ # 전산업무비 (4개)
│ │ │ ├── index.vue # 전산업무비 목록
│ │ │ ├── [id].vue # 전산업무비 상세
│ │ │ ├── form.vue # 전산업무비 등록/수정 폼
│ │ │ └── terminal.vue # 단말기 관리 페이지
│ │ │
│ │ ├── documents/ # 요구사항 정의서 (5개)
│ │ │ ├── index.vue # 요구사항 정의서 목록
│ │ │ ├── [id].vue # 요구사항 정의서 상세 뷰
│ │ │ ├── form.vue # 요구사항 정의서 등록/수정 폼 (Tiptap)
│ │ │ ├── review-[id].vue # 요구사항 정의서 사전협의 페이지
│ │ │ └── council-request/ # 협의회 신청 (서브 페이지)
│ │ │
│ │ ├── plan/ # 정보기술부문 계획 (3개)
│ │ │ ├── index.vue # 정보기술부문 계획 목록
│ │ │ ├── [id].vue # 정보기술부문 계획 상세
│ │ │ └── form.vue # 정보기술부문 계획 등록/수정 폼
│ │ │
│ │ └── projects/ # 정보화사업 (4개)
│ │ ├── index.vue # 정보화사업 목록
│ │ ├── [id].vue # 정보화사업 상세 (프로젝트 개요/진행/자원)
│ │ ├── form.vue # 정보화사업 등록/수정 폼 (Tiptap)
│ │ └── report.vue # 정보화사업 보고서 조회/PDF 생성
├── plugins/ # Nuxt 플러그인 (1개)
│ └── auth.ts # $apiFetch 전역 제공 (인증 인터셉터, 401 갱신 처리)
│
├── stores/ # Pinia 상태관리 (2개)
│ ├── auth.ts # 인증 상태 (로그인/로그아웃/갱신/세션 복원, it-portal-user 쿠키 기반)
│ └── review.ts # 사전협의 세션 상태 (현재 문서/버전/코멘트/검토자, 메모리 저장)
│
├── types/ # TypeScript 타입 정의 (5개)
│ ├── auth.ts # User/UserRole/ROLE 상수 (ADMIN/USER/DEPT_MANAGER)
│ ├── budget-work.ts # 예산 작업 타입 (편성비목/편성결과)
│ ├── budgetStatus.ts # 예산현황 조회 응답 타입
│ ├── council.ts # 정보화실무협의회 타입 (CouncilItem/위원/평가 등)
│ └── review.ts # 사전협의 타입 (ReviewSession/Comment/Reviewer 등)
│
└── utils/ # 유틸리티 함수 (3개)
├── common.ts # 공통 유틸 (formatBudget, getApprovalTagClass, getProjectTagClass, 전결권 계산)
├── excel.ts # Excel 내보내기 (ExcelJS 기반)
└── hwpx.ts # HTML→HWPX 변환 (HWP 파일 구조 분석, 이미지 패키징)
설정: nuxt.config.ts의 ssr: false
배경:
- 초기 SSG(
npm run generate) + nginx에서 SSO 무한 리다이렉트 발생 - 원인: 빌드 시점 Pinia 스토어
user=null이_payload.json에 직렬화 → 클라이언트 하이드레이션 시it-portal-user쿠키 기반 복원을null로 덮어쓰는 타이밍 이슈 - 사내 포털이라 SEO 불필요 → CSR(완전 클라이언트 렌더링)이 최적
결과:
- 모든 라우트가 동일
index.html에서 클라이언트 라우팅 - 서버 미들웨어/페이로드 생성 불필요
| 패턴 | 구현체 | 용도 | 인증/401 처리 |
|---|---|---|---|
useApiFetch<T> |
Nuxt useFetch 래핑 |
GET 요청 (조회) | credentials: 'include' + 401 시 refresh 후 재호출 |
$apiFetch |
ofetch 래핑 (plugin/auth.ts) |
POST/PUT/DELETE 요청 (변경) | 인터셉터로 credentials: 'include' + 401 처리 |
설계 이유:
useFetch: Nuxt 내장이므로 SSR/CSR 양쪽 호환, 캐싱과 중복 호출 방지가 자동. GET 요청에 최적화$apiFetch:ofetch기반으로 간단한 POST/PUT/DELETE 변경 요청 처리, 플러그인으로 인증 로직 중앙화
주의: stores/auth.ts 내부에서는 $apiFetch 사용 불가 (순환 참조). 대신 Nuxt 내장 $fetch를 직접 사용.
토큰 저장:
- 서버가
Set-Cookie헤더로 httpOnly 쿠키에 JWT(accessToken, refreshToken) 저장 - JavaScript에서 직접 접근 불가 → XSS 시 토큰 탈취 방지
- 프론트는
credentials: 'include'만 설정하면 브라우저가 자동 전송
인증 상태 관리:
- 사용자 정보는 별도 쿠키(
it-portal-user)에 JSON 문자열로 저장 - 로그인 후:
useCookie('it-portal-user')로 상태 복원 - 새로고침/탭 전환: 쿠키 기반으로 자동 재인증
- 401 응답:
plugin/auth.ts의 인터셉터에서 refresh 호출 → 새 토큰 쿠키 세팅 → 원요청 재시도
문제:
- Nuxt 하이드레이션 중 테마가 바뀌면 화면 깜빡임 발생
해결:
nuxt.config.ts의app.head.script인라인 스크립트가 하이드레이션 이전에 실행theme-dark쿠키 읽기 →<html class="dark">+color-scheme즉시 적용- SSR 없으므로 클라이언트 초기 렌더링부터 올바른 테마 적용
문제: 6개 폼 필드(주관부서/IT부서/팀장/담당자 등)에서 같은 직원 검색 다이얼로그를 사용하되, 선택 시 각 필드에 맞게 바인딩해야 함
해결:
// 1. 어떤 필드에서 열었는지 추적
const activeDialogField = ref<'svnDpm' | 'itDpm' | ...>('svnDpm');
// 2. 필드별 헤더 derived from activeDialogField
const FIELD_HEADERS = { svnDpm: '주관부서 검색', svnDpmTlr: '팀장 검색', ... } as const;
const dialogHeader = computed(() => FIELD_HEADERS[activeDialogField.value]);
// 3. 선택 시 FIELD_CONFIG 매핑으로 폼 데이터에 세팅
const FIELD_CONFIG = {
svnDpm: { valueKey: 'orgCode', labelKey: 'bbrNm' }, // 부서코드/부서명
svnDpmTlr: { valueKey: 'eno', labelKey: 'usrNm' }, // 직원번호/이름
itDpm: { valueKey: 'orgCode', labelKey: 'bbrNm' }, // IT 부서코드
itDpmTlr: { valueKey: 'eno', labelKey: 'usrNm' }, // IT 팀장 직원번호
};
// 4. 선택 후
const onEmployeeSelected = (employee) => {
const config = FIELD_CONFIG[activeDialogField.value];
formData[activeDialogField.value] = employee[config.valueKey];
formData[activeDialogField.value + 'Nm'] = employee[config.labelKey];
};구현: composables/useEmployeeSearch.ts + EmployeeSearchDialog.vue
규칙: 모든 v-html 사용 전에 isomorphic-dompurify로 새니타이징
<!-- 금지 -->
<div v-html="richTextContent" />
<!-- 필수 -->
<div v-html="DOMPurify.sanitize(richTextContent)" />적용 대상:
prjDes,prjRng(정보화사업 설명/범위)- 요구사항 정의서/가이드 문서의 HTML 콘텐츠
- 검토의견 렌더링
20개 이상의 확장으로 완전한 리치 텍스트 편집 지원:
| 범주 | 확장 | 용도 |
|---|---|---|
| 기본 | StarterKit | 문단, 제목, 굵음, 기울임, 코드 블록 등 |
| 표 | Table, TableCell, TableRow, TableHeader | 표 삽입/편집 + 너비 저장 커스텀 |
| 이미지 | Image, ResizableImage (커스텀) | 이미지 삽입 + 크기 조절 + base64/API 업로드 |
| 다이어그램 | ExcalidrawExtension (커스텀) | Excalidraw 다이어그램 노드로 통합 |
| 수식 | InlineMath, BlockMath (커스텀 + mathlive) | LaTeX 수식 렌더링 |
| 파일 | AttachmentNode (커스텀) | 첨부파일 노드 및 다운로드 |
| 포맷 | TextStyle, Color, Highlight, TextAlign | 색상, 배경색, 정렬 |
| 유틸 | CharacterCount, Placeholder | 글자 수 제한, 플레이스홀더 |
| Task | TaskList, TaskItem | 체크리스트 |
상태 관리:
useExcalidrawDialog()composable로 NodeView와 Editor 간 통신- 이미지/파일 업로드는
useFiles.ts+useTiptapImageInsertion.ts로 처리
목차(TOC) 생성:
- Heading 노드에 자동
id부여 @update:toc이벤트로 부모 페이지에 전달- 가이드 문서/요구사항 정의서에서 자동 생성
구현: utils/hwpx.ts (대규모 유틸리티)
프로세스:
HTML 문자열 (Tiptap 에디터 콘텐츠)
↓ HTML 파싱 및 DocNode로 변환
↓ ParagraphNode (텍스트/포맷), TableNode (셀/병합) 생성
↓ 이미지를 Base64 또는 파일에서 로드 (utils/hwpx-images.ts)
↓ HWP XML 구조 생성 (utils/hwpx-package-xml.ts)
↓ JSZip으로 압축
↓ HWPX 파일 다운로드
포맷:
- 폰트: 맑은 고딕 (본문 10pt, H1 15pt, H3 12pt)
- 표: th/td 구분, 셀 병합, 파란 배경 헤더, 균등 열배분
- 이미지: 크기 조절 적용
테스트: tests/unit/utils/hwpx.test.ts에서 HTML 파싱, 노드 변환, XML 생성 검증
1. 클라이언트 라우트 가드 (UX):
// middleware/admin.ts
export default defineRouteMiddleware((to, from) => {
const { user } = useAuth();
if (!user.value?.athIds?.includes(ROLE.ADMIN)) {
return navigateTo('/');
}
});2. 페이지 레이아웃 (렌더링):
// pages/admin/*.vue
definePageMeta({ layout: 'admin' });3. 백엔드 API 보안 (진정한 보호):
// it_backend/SecurityConfig
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// 각 컨트롤러
@PreAuthorize("hasRole('ADMIN')")
@RestController
@RequestMapping("/api/admin")
public class AdminController { ... }관리 대상: 공통코드, 사용자, 역할, 자격등급, 조직, 파일, 토큰, 감사로그 (8개 도메인)
상태 관리: stores/review.ts (Pinia, 메모리 저장)
구조:
- 세션: 현재 열린 문서 ID, 검토자, 작성자
- 버전: v0.0(초안) → v0.1, v0.2... (검토요청마다 스냅샷)
- 코멘트: 인라인 코멘트 (Tiptap Mark 기반, 텍스트 구간에 부착)
- 검토자: 역할 구분 (author, reviewer)
주의점 (TASK.md):
- 세션/코멘트 서버 영속화 필요 (현재 메모리만 저장, 새로고침 시 손실)
- 프로젝트별 검토자 목록을 서버에서 조회 (현재 하드코딩)
- 검토의견 DTO에 작성자 팀명/첨부파일 필드 추가 필요
용도: 특정 시점의 정보화사업 계획서 관리
구조:
plans테이블에 CRUD 지원planDetailsJSON 필드에 사업 목록 직렬화 (JSON 배열)- 계획 상세 조회 시 프로젝트 목록 함께 반환
구현: composables/usePlan.ts + pages/info/plan/
워크플로우:
- 결재 완료된 예산 → 편성 대상
- 편성비목별로 편성률(0~100%) 입력
pages/budget/work.vue에서 일괄 계산- BBUGTM 테이블 upsert
타입: types/budget-work.ts (편성비목, 편성결과)
프로세스: 신청 → 타당성검토 → 위원선정 → 일정확정 → 사전질의 → 평가 → 결과서
컴포넌트 그룹:
- committee: 위원 선정 (CommitteeSelector, CommitteeList)
- evaluation: 평가 (EvaluationForm, EvalSummaryPanel)
- feasibility: 타당성검토 (4개: 개요, 체크리스트, 성과, 폼)
- schedule: 일정 (ScheduleInput, ScheduleStatus)
- qna: 사전질의 (CouncilQna)
- notice: 공지 (CouncilNotice)
- result: 결과서 (ResultForm, ResultReview)
상태 관리: useCouncil.ts + useCouncilCodes.ts (상태/심의 유형 코드)
1. 로그인 → stores/auth.ts login() → 서버가 httpOnly JWT 쿠키 세팅 + user 정보는 it-portal-user 쿠키 저장
2. API 요청 → useApiFetch / $apiFetch → credentials:'include'로 httpOnly 쿠키 자동 전송
3. 401 응답 → onResponseError → refresh() 호출 → 서버가 새 쿠키 세팅 → tokenRefreshSignal++로 재요청
4. 갱신 실패 → logout() → user 쿠키 및 구버전 localStorage 잔존 데이터 정리 → /login 리다이렉트
5. 새로고침 → useCookie → SSR/클라이언트에서 it-portal-user 쿠키 기반 인증 상태 복원
6. 구버전 데이터 → restoreSession() → localStorage user가 남아 있으면 user 쿠키로 1회 마이그레이션
| 변수명 | 설명 | 기본값 | 설정 방식 |
|---|---|---|---|
NUXT_PUBLIC_API_BASE |
백엔드 API 서버 주소 | http://localhost:18080 |
.env 파일 또는 환경변수 |
# 의존성 설치
npm install
# Nuxt 타입 정의 생성 (.nuxt/ 디렉토리)
npm run postinstall
# 또는
npx nuxt prepare타입 오류 발생 시:
- IDE에서
.nuxt/디렉토리 체크 npm run postinstall다시 실행node_modules/.nuxt삭제 후 재설치
# 개발 서버 (기본 포트 3000, HMR 활성화)
npm run dev
# 브라우저 자동 열기
http://localhost:3000백엔드 서버 연동:
- 동시에
cd ../it_backend && ./gradlew bootRun실행 (포트 8080) - 로그인 페이지에서 정상 작동 확인
# CSR(SPA) 정적 생성
npm run generate
# 결과: dist/ 디렉토리
# nginx 또는 Apache에서 index.html 기본 페이지로 설정하여 라우팅 처리목표: 80% 이상의 코드 커버리지 유지 (단위 + 통합 + E2E)
# 전체 단위 테스트 실행
npm test
# 전체 단위 테스트 실행 (UI 모드)
npm run test:ui
# 파일 변경 감지 + 자동 재실행 (개발 중 사용)
npm run test:watch
# 코드 커버리지 리포트 생성 (coverage/ 디렉토리)
npm run test:coverage# Playwright 브라우저 설치 (최초 1회)
npx playwright install
# E2E 테스트 실행 (dev 서버 자동 기동)
npm run test:e2e
# Playwright 인터랙티브 UI 모드 (테스트 디버깅)
npm run test:e2e:uitests/
├── unit/ # Vitest 단위 테스트 (12파일, 248+ 케이스)
│ ├── utils/
│ │ └── common.test.ts # formatBudget, 상태 태그, API 오류 메시지 등
│ ├── stores/
│ │ ├── auth.test.ts # login, logout, restoreSession, refresh, 쿠키 처리
│ │ └── review.test.ts # 사전협의 세션/버전/코멘트 상태 관리
│ └── composables/
│ ├── useApiFetch.test.ts # GET 래퍼, 인증 처리
│ ├── useAuth.test.ts # Pinia 래퍼 composable
│ ├── useCost.test.ts # 전산업무비 CRUD
│ ├── useProjects.test.ts # 정보화사업 CRUD
│ ├── useBudgetStatus.test.ts # 예산현황 3탭 조회
│ ├── useCouncil.test.ts # 협의회 CRUD
│ ├── useApprovalDashboard.test.ts # KPI 대시보드
│ ├── useDocumentDashboard.test.ts # 사전협의 대시보드
│ ├── usePendingApprovalCount.test.ts # 배지 카운트
│ ├── useDateRangeValidation.test.ts # 날짜 범위 검증
│ └── (추가 작성 필요)
│
└── e2e/ # Playwright E2E 테스트
├── auth.setup.ts # 로그인 인증 설정 (모든 E2E의 사전 조건)
├── helpers/
│ └── mockApi.ts # 공통 Mock 헬퍼 (mockLoginApi, mockApi, setLoggedIn)
├── auth.spec.ts # 로그인 성공/실패 플로우
├── projects.spec.ts # 정보화사업 목록 조회
├── cost.spec.ts # 전산업무비 목록 조회
└── approval.spec.ts # 전자결재 목록 조회
단위 테스트 (Vitest):
// $fetch 전역 Mock
const mockFetch = vi.fn();
vi.stubGlobal('$fetch', mockFetch);
// 클라이언트 환경 시뮬레이션 (SSR 없음)
Object.assign(process, { client: true });
// Nuxt auto-import은 지원 안 함 → 명시적 import
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
import { useRuntimeConfig } from '#app'; // Mock 필요E2E 테스트 (Playwright):
// API 응답 Mock
await page.route('**/api/projects', (route) =>
route.fulfill({
status: 200,
body: JSON.stringify(mockData)
})
);
// 사전 인증 쿠키 주입 (백엔드 없이 실행)
await page.addInitScript(() => {
document.cookie = 'it-portal-user=' + JSON.stringify({
eno: '123456',
empNm: 'Test User',
athIds: ['ITPZZ001']
});
});Nuxt auto-import 미지원:
- Vitest에서
#app,#imports경로 해석 불가 ref,computed,defineStore등은'vue'/'pinia'에서 직접 importuseRuntimeConfig,navigateTo,useNuxtApp등은vi.stubGlobal()로 Mock
E2E 테스트 안정성:
waitForURL(),waitForLoadState('networkidle')사용- setTimeout 대신 deterministic 대기 (요소 가시성, 네트워크 완료)
- 테스트 순서 독립성 보장 (각 테스트마다 신선한 컨텍스트)
백엔드(it_backend)는 도메인 기반 레이어드 아키텍처(Spring Boot)를 사용합니다.
프론트엔드는 아래 API 경로를 기준으로 composable을 분리합니다.
| 도메인 | 백엔드 패키지 | 프론트 Composable | API 경로 |
|---|---|---|---|
| 인증 | common/system |
useAuth.ts |
/api/auth/** |
| 사용자/조직 | common/iam |
useEmployeeSearch.ts, useOrganization.ts |
/api/users/**, /api/organizations |
| 신청서/결재 | common/approval |
useApprovals.ts, useApprovalDashboard.ts |
/api/applications/** |
| 공통코드 | common/code |
useCodeOptions.ts |
/api/ccodem/** |
| 시스템 관리 | common/admin |
useAdminApi.ts |
/api/admin/** |
| 정보화사업 | budget/project |
useProjects.ts, useProjectOptions.ts |
/api/projects/** |
| 전산업무비 | budget/cost |
useCost.ts |
/api/costs/** |
| 가이드 문서 | budget/document |
useGuideDocuments.ts |
/api/guide-documents/** |
| 정보기술부문 계획 | budget/plan |
usePlan.ts |
/api/plans/** |
| 예산 작업 | budget/work |
- (직접 호출) | /api/budget/work/** |
| 예산현황 | budget/status |
useBudgetStatus.ts |
/api/budget/status/** |
| 요구사항 정의서 | budget/document |
useDocuments.ts, useDocumentDashboard.ts |
/api/documents/** |
| 검토의견(코멘트) | budget/document |
useReviewCommentApi.ts |
/api/documents/{id}/review-comments/** |
| 정보화실무협의회 | council |
useCouncil.ts, useCouncilCodes.ts |
/api/council/** |
| 파일 관리 | infra/file |
useFiles.ts |
/api/files/** |
| Gemini AI | infra/ai |
- (직접 호출) | /api/gemini/generate |
상세 정보:
- 모든 인증된 API 요청은 httpOnly 쿠키 기반 (
credentials: 'include') - 백엔드 SoT:
../it_backend/CLAUDE.md(인증 정책, 보안 설정) - API 응답 포맷: 표준 JSON (성공/실패 필드, 데이터, 메타데이터 포함)
- 모든 코드 주석은 한국어로 작성
- public API, service 메서드, composable 반환 함수에는 입력값과 실패 조건 기록
- 간단한 할당/트리비얼 메서드에는 주석 불필요
- TODO/FIXME는 후속 조치 가능한 문장으로 작성 (장기 과제는
TASK.md에 등록)
- Composition API +
<script setup lang="ts">필수 - 반응형:
ref(),computed(),reactive()적절히 선택 - Props/Emits는 명시적 타입 정의
v-html사용 시 반드시DOMPurify.sanitize()적용
| 메서드 | 사용처 | 예시 |
|---|---|---|
useApiFetch<T> |
GET 조회 | const { data, pending } = useApiFetch('/api/projects') |
$apiFetch |
POST/PUT/DELETE | await $apiFetch('/api/projects', { method: 'POST', body }) |
$fetch |
인증 API 내부 (stores/auth.ts) | await $fetch('/api/auth/login', { credentials: 'include' }) |
- PrimeVue 우선: Dialog, Dropdown, DataTable, Button 등
- Tailwind CSS: 여백, 텍스트 정렬, 기타 레이아웃
- 공통 컴포넌트 활용: StyledDataTable.vue, EmployeeSearchDialog.vue 등
components/common/자동 등록 시Common접두사 생략, 명시적 import 사용
// ✓ 명시적 타입 정의
interface ProjectFormData {
prjNm: string;
prjDes: string;
startDt: Date;
}
const formData = ref<ProjectFormData>({ ... });
// ✓ 함수 매개변수/반환 타입
function formatBudget(value: number): string { ... }
// ✗ 금지
const data: any = { ... };- 최대 파일 크기: 800줄 (초과 시 기능별로 분할)
- 함수 최대 크기: 50줄 (초과 시 헬퍼 함수로 추출)
- 깊은 중첩: 4 레벨 초과 금지 (early return 사용)
- 불변성: 객체/배열 직접 수정 금지, 스프레드 연산자 사용
try {
const data = await useApiFetch<Project[]>('/api/projects');
// 처리
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
// 로깅 및 사용자 피드백
}- 전체 규칙:
../CLAUDE.md(한글 주석, 인증 정책 등) - CLAUDE.md 이외 규칙:
CLAUDE.md§4 (기술 스택, API 패턴, 타입 충돌 등) - 테스트 작성: CLAUDE.md §4.10
| 날짜 | 항목 | 비고 |
|---|---|---|
| 2026-04-29 | README 최신화 | 인증 흐름(useCookie/httpOnly), 다크모드 쿠키, 테스트 현황, 모듈 목록, 백엔드 API 기준일 보정 |
| 2026-04-25 | 전체 프로젝트 문서/주석 리프레시 | console.log 제거(usePdfReport, report.vue), README/CLAUDE/TASK.md 최신화 |
| 2026-04-24 | 대시보드 홈 카드 스타일 개선 | /info/documents, /info, /approval, /admin/dashboard — Narrative·Segmented 스타일 적용 |
| 2026-04-24 | 대시보드 Composable 신규 구현 | useApprovalDashboard, useDocumentDashboard (KPI + 월별 통계 + 배지 카운트) |
| 2026-04-24 | 예산현황·검토의견 백엔드 도메인 추가 | budget/status (BudgetStatusService 3탭), budget/document (Brivgm 엔티티, ReviewCommentService) |
| 2026-04-10 | 전체 프로젝트 문서/주석 리프레시 | Task 1~4 기반 전수 점검: 주석 보강(11개 파일), README/CLAUDE/TASK.md 최신화 |
| 2026-04-09 | 프로젝트 문서 및 주석 리프레시 | 워크플로우 기반 전체 소스 코드 주석 보강 및 문서 최신화 |
| 2026-04-05 | 정보화실무협의회 모듈 구현 | 15개 컴포넌트, CouncilController(23 엔드포인트), 8개 서비스 |
| 2026-04-04 | 시스템 관리자 모듈 구현 | admin 레이아웃/미들웨어, 10개 관리 페이지, useAdminApi(24 CRUD) |
| 2026-04-04 | 예산 작업(편성률 적용) 구현 | budget/work.vue, BudgetWorkController(3 API) |
| 2026-04-02 | 사전협의(문서 검토) 모듈 구현 | review 스토어/composable/5개 컴포넌트, Tiptap Mark 기반 인라인 코멘트 |
| 2026-04-02 | 정보기술부문 계획 모듈 구현 | plan CRUD(목록/상세/등록), PlanController/Service |
| 2026-04-02 | Playwright E2E 테스트 안정화 | E2E 테스트 시간 초과 및 인증 문제 해결, API 모킹 개선 |
| 2026-03-30 | Tiptap 수식 및 컴포넌트 고도화 | LaTeX 수식 삽입, 표 정렬, 파일 첨부 컴포넌트 안정성 개선 |
| 2026-03-28 | 프로젝트 화면 UX 개선 | Date 유효성 검사 추가 및 다크 모드 테이블 색상 렌더링 수정 |
| 2026-03-27 | 백엔드 domain-refactor TypeScript 동기화 | 백엔드 패키지 구조 domain 기반 전환에 따른 타입 정의 확인 |
| 2026-03-25 | 전체 프로젝트 문서화 리프레시 | 소스 코드 주석 전수 점검(60개 파일), README.md 최신화 |
| 2026-03-09 | httpOnly 쿠키 인증 전환 | stores/auth.ts, plugins/auth.ts, useApiFetch.ts 전면 개편 |
| 2026-03-08 | 프론트엔드 테스트 환경 구축 | Vitest + Playwright 기반 테스트 환경 도입 |
| 2026-03-04 | 직원 검색 다이얼로그 연동 | form.vue 6개 필드(주관/IT 부서·팀장·담당자) |
| 2026-03-02 | 예산 통합 목록 "전체" 탭 | budget/list.vue UnifiedBudgetItem 구현 |
| 주제 | 파일 | 용도 |
|---|---|---|
| 공통 규약 | ../CLAUDE.md |
인증 정책(SoT), 한글 주석 원칙, 팀 규칙 |
| 프론트 가이드 | ./CLAUDE.md |
기술 스택, API 패턴, 타입 정의, 테스트 작성 기준 |
| 기술 부채 | ../TASK.md |
미구현 항목, 보안 강화 과제, 회귀 테스트 범위 |
| 설정 | nuxt.config.ts |
Nuxt 4 설정 (ssr: false, runtimeConfig, 보안 헤더) |
| 환경변수 | .env, .env.local |
NUXT_PUBLIC_API_BASE (백엔드 주소) |
| 전역 스타일 | app/assets/css/main.css |
다크모드, 색상 유틸리티, 커스텀 클래스 |
| 공통 컴포넌트 | app/components/common/ |
StyledDataTable, EmployeeSearchDialog 등 |
# 1. pages/ 디렉토리에 .vue 파일 생성
# 2. Nuxt가 자동으로 라우팅 생성 (파일명 = 경로)
# 3. 필요한 composable import
# 4. 미들웨어가 필요하면 definePageMeta에 등록
definePageMeta({ middleware: 'admin' });
# 5. 테스트 파일 생성 (tests/unit/composables/ 또는 tests/e2e/)// composables/useNewFeature.ts
import { useApiFetch } from './useApiFetch';
export const useNewFeature = () => {
const { data, pending } = useApiFetch<NewFeatureDto>('/api/new-feature');
const create = async (payload) => {
const { $apiFetch } = useNuxtApp();
return await $apiFetch('/api/new-feature', {
method: 'POST',
body: payload
});
};
return { data, pending, create };
};// stores/newStore.ts
import { defineStore } from 'pinia';
export const useNewStore = defineStore('newStore', () => {
const state = ref<State>(initialState);
const action = () => {
// 로직
};
return { state, action };
});// tests/unit/components/MyComponent.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import MyComponent from '~/components/MyComponent.vue';
describe('MyComponent', () => {
it('renders props correctly', () => {
const wrapper = mount(MyComponent, { props: { title: 'Test' } });
expect(wrapper.text()).toContain('Test');
});
});# 1. .nuxt/ 디렉토리 재생성
npm run postinstall
# 2. IDE 재시작
# 3. 여전히 오류 시
rm -rf node_modules/.nuxt
npm run postinstall# 1. 백엔드 서버 실행 여부 확인
curl http://localhost:8080/api/auth/me
# 2. 로그인 재시도
# 브라우저 개발자 도구 > Application > Cookies 확인
# 3. 환경변수 확인
echo $NUXT_PUBLIC_API_BASE # 기본값: http://localhost:8080# 빌드 메모리 증가
NODE_OPTIONS=--max-old-space-size=8192 npm run build
# 또는 package.json에 설정되어 있음
npm run build # cross-env NODE_OPTIONS 자동 적용# timeout 값 확가 (tests/e2e/)
await page.waitForLoadState('networkidle');
await page.waitForURL('**/projects');
# 또는 명시적 대기
await page.waitForSelector('table');