feat: React 18 프론트엔드 추가 및 프로젝트 구조 정리
- wtm-frontend → wtm-frontend-vue 이름 변경 - wtm-frontend-react 추가 (React 18 + PrimeReact + Zustand) - 동일한 모듈 구조 및 API 연동 (Vue 버전과 기능 동일) - Vue:5173 / React:5174 포트 분리 - 개발자 가이드에 React 프론트엔드 안내 추가 - .gitignore: Claude/OMC, 문서 생성 스크립트, package-lock 제외 - 불필요 파일 git 추적 제거 (.omc, generate_*.py, regenerate_*.py) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
import BaseFormDialog from '@/core/components/BaseFormDialog.vue';
|
||||
import type { Project } from '../project.types';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
project: Partial<Project> | null;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
save: [data: Partial<Project>];
|
||||
}>();
|
||||
|
||||
const form = ref<Partial<Project>>({});
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'EPC', value: 'EPC' },
|
||||
{ label: 'PMC', value: 'PMC' },
|
||||
{ label: 'FEED', value: 'FEED' },
|
||||
{ label: 'Other', value: 'OTHER' },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '진행중', value: 'ACTIVE' },
|
||||
{ label: '종료', value: 'CLOSED' },
|
||||
{ label: '보류', value: 'HOLD' },
|
||||
];
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (v) {
|
||||
form.value = props.project ? { ...props.project } : { status: 'ACTIVE' };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function onSubmit() {
|
||||
emit('save', { ...form.value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseFormDialog
|
||||
:visible="visible"
|
||||
:title="project?.id ? '프로젝트 수정' : '프로젝트 등록'"
|
||||
width="680px"
|
||||
:loading="loading"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<div class="form-grid">
|
||||
<div class="col-4">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">프로젝트 코드</label>
|
||||
<InputText v-model="form.projectCode" placeholder="EPU-2025-001" fluid />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">프로젝트명</label>
|
||||
<InputText v-model="form.name" placeholder="프로젝트명 입력" fluid />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">유형</label>
|
||||
<Select v-model="form.projectType" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="선택" fluid />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">상태</label>
|
||||
<Select v-model="form.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="선택" fluid />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label">시작일</label>
|
||||
<DatePicker :modelValue="form.startDate ? new Date(form.startDate) : null" dateFormat="yy-mm-dd" placeholder="YYYY-MM-DD" fluid @update:modelValue="form.startDate = $event ? ($event as Date).toISOString().slice(0, 10) : undefined" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label">종료일</label>
|
||||
<DatePicker :modelValue="form.endDate ? new Date(form.endDate) : null" dateFormat="yy-mm-dd" placeholder="YYYY-MM-DD" fluid @update:modelValue="form.endDate = $event ? ($event as Date).toISOString().slice(0, 10) : undefined" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseFormDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
export const projectRoutes: RouteRecordRaw[] = [
|
||||
{ path: '/projects', name: 'project-list', component: () => import('./views/ProjectListView.vue'), meta: { title: '프로젝트' } },
|
||||
{ path: '/projects/:id', name: 'project-detail', component: () => import('./views/ProjectDetailView.vue'), meta: { title: '프로젝트 상세' } },
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
import api from '@/core/api/axios';
|
||||
const BASE = '/api/wtm/projects';
|
||||
export const projectService = {
|
||||
getAll: (params?: Record<string, unknown>) => api.get(BASE, { params }),
|
||||
create: (data: unknown) => api.post(BASE, data),
|
||||
getById: (id: number) => api.get(`${BASE}/${id}`),
|
||||
update: (id: number, data: unknown) => api.put(`${BASE}/${id}`, data),
|
||||
getMy: () => api.get(`${BASE}/my`),
|
||||
getMembers: (id: number) => api.get(`${BASE}/${id}/members`),
|
||||
addMember: (id: number, data: unknown) => api.post(`${BASE}/${id}/members`, data),
|
||||
getAssignments: (id: number) => api.get(`${BASE}/${id}/assignments`),
|
||||
createAssignment: (id: number, data: unknown) => api.post(`${BASE}/${id}/assignments`, data),
|
||||
updateAssignment: (id: number, aid: number, data: unknown) => api.put(`${BASE}/${id}/assignments/${aid}`, data),
|
||||
deleteAssignment: (id: number, aid: number) => api.delete(`${BASE}/${id}/assignments/${aid}`),
|
||||
getAvailable: (id: number) => api.get(`${BASE}/${id}/assignments/available`),
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
export const useProjectStore = defineStore('project', () => {
|
||||
const projects = ref<unknown[]>([]);
|
||||
const loading = ref(false);
|
||||
return { projects, loading };
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export interface Project { id: number; projectCode: string; name: string; projectType: string; status: string; startDate?: string; endDate?: string; pmUserId?: number; }
|
||||
@@ -0,0 +1 @@
|
||||
<template><div class="card"><h1>프로젝트 상세</h1></div></template>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Column from 'primevue/column';
|
||||
import Button from 'primevue/button';
|
||||
import Tag from 'primevue/tag';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import BasePageHeader from '@/core/components/BasePageHeader.vue';
|
||||
import BaseCrudTable from '@/core/components/BaseCrudTable.vue';
|
||||
import ProjectFormDialog from '../components/ProjectFormDialog.vue';
|
||||
import { projectService } from '../project.service';
|
||||
import { PROJECT_STATUS } from '@/core/constants/app.constants';
|
||||
import type { Project } from '../project.types';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const projects = ref<Project[]>([]);
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogLoading = ref(false);
|
||||
const selectedProject = ref<Partial<Project> | null>(null);
|
||||
|
||||
async function loadProjects() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await projectService.getAll();
|
||||
projects.value = (data as any).items ?? data;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: '프로젝트 목록 로드 실패', life: 5000 });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
selectedProject.value = null;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(project: Project) {
|
||||
selectedProject.value = { ...project };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function onSave(data: Partial<Project>) {
|
||||
dialogLoading.value = true;
|
||||
try {
|
||||
if (data.id) {
|
||||
await projectService.update(data.id, data);
|
||||
toast.add({ severity: 'success', summary: '성공', detail: '프로젝트가 수정되었습니다.', life: 3000 });
|
||||
} else {
|
||||
await projectService.create(data);
|
||||
toast.add({ severity: 'success', summary: '성공', detail: '프로젝트가 등록되었습니다.', life: 3000 });
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
await loadProjects();
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: '저장 실패', life: 5000 });
|
||||
} finally {
|
||||
dialogLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function statusSeverity(status: string) {
|
||||
return (PROJECT_STATUS as Record<string, any>)[status]?.severity ?? 'secondary';
|
||||
}
|
||||
function statusLabel(status: string) {
|
||||
return (PROJECT_STATUS as Record<string, any>)[status]?.label ?? status;
|
||||
}
|
||||
|
||||
function typeTag(type: string) {
|
||||
const map: Record<string, string> = { EPC: 'info', PMC: 'warn', FEED: 'success', OTHER: 'secondary' };
|
||||
return map[type] ?? 'secondary';
|
||||
}
|
||||
|
||||
onMounted(loadProjects);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="project-list-view">
|
||||
<BasePageHeader title="프로젝트 목록" subtitle="프로젝트 조회 및 관리">
|
||||
<template #actions>
|
||||
<Button label="등록" icon="pi pi-plus" @click="openCreate" />
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<BaseCrudTable
|
||||
:value="projects"
|
||||
:loading="loading"
|
||||
:globalFilterFields="['projectCode', 'name', 'projectType', 'status']"
|
||||
@row-select="openEdit"
|
||||
>
|
||||
<Column field="projectCode" header="프로젝트코드" sortable style="min-width: 140px" />
|
||||
<Column field="name" header="이름" sortable style="min-width: 180px" />
|
||||
<Column field="projectType" header="유형" sortable style="min-width: 90px">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="data.projectType" :severity="typeTag(data.projectType)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="상태" sortable style="min-width: 90px">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="statusLabel(data.status)" :severity="statusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="pmUserId" header="PM" sortable style="min-width: 100px" />
|
||||
<Column header="기간" style="min-width: 180px">
|
||||
<template #body="{ data }">
|
||||
{{ data.startDate ?? '-' }} ~ {{ data.endDate ?? '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="" style="width: 80px">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-pencil" text rounded severity="info" @click="openEdit(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</BaseCrudTable>
|
||||
|
||||
<ProjectFormDialog
|
||||
:visible="dialogVisible"
|
||||
:project="selectedProject"
|
||||
:loading="dialogLoading"
|
||||
@update:visible="dialogVisible = $event"
|
||||
@save="onSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
새 Issue에서 참조
사용자 차단