|
|
|
@ -5,7 +5,6 @@ import { useAutoflowStore } from "@/stores/autoflowStore";
|
|
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
import type {
|
|
|
|
UiProject,
|
|
|
|
UiProject,
|
|
|
|
ApiProject,
|
|
|
|
|
|
|
|
Permission,
|
|
|
|
Permission,
|
|
|
|
} from "@/components/models/project/Project";
|
|
|
|
} from "@/components/models/project/Project";
|
|
|
|
import { ProjectService } from "@/components/service/project/projectService";
|
|
|
|
import { ProjectService } from "@/components/service/project/projectService";
|
|
|
|
@ -31,6 +30,11 @@ const menuX = ref(0);
|
|
|
|
const menuY = ref(0);
|
|
|
|
const menuY = ref(0);
|
|
|
|
const selectedIndex = ref<number | null>(null);
|
|
|
|
const selectedIndex = ref<number | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// id별 원본 등록자(reg) 보관 (값 그대로, 가공/문자열화 X)
|
|
|
|
|
|
|
|
const projectRegById = ref<Record<number, { regId?: string; regNm?: string }>>(
|
|
|
|
|
|
|
|
{},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const projects = ref<UiProject[]>([]);
|
|
|
|
const projects = ref<UiProject[]>([]);
|
|
|
|
type UserOption = { id: number | string; username: string };
|
|
|
|
type UserOption = { id: number | string; username: string };
|
|
|
|
const userOptions = ref<UserOption[]>([]);
|
|
|
|
const userOptions = ref<UserOption[]>([]);
|
|
|
|
@ -43,7 +47,8 @@ const form = ref({
|
|
|
|
prjDesc: "",
|
|
|
|
prjDesc: "",
|
|
|
|
selectedUsers: [] as string[],
|
|
|
|
selectedUsers: [] as string[],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
/** ===== 서버 응답 타입 ===== */
|
|
|
|
|
|
|
|
|
|
|
|
/** ===== 롤 ===== */
|
|
|
|
const roles = ref<string[]>([]);
|
|
|
|
const roles = ref<string[]>([]);
|
|
|
|
const refreshRoles = () => {
|
|
|
|
const refreshRoles = () => {
|
|
|
|
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
|
|
|
|
const auth = storage.getAuth?.() ?? storage.get?.("vpp-Auth") ?? null;
|
|
|
|
@ -52,27 +57,93 @@ const refreshRoles = () => {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
|
|
|
|
const isAdmin = computed(() => roles.value.includes("ROLE_ADMIN"));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** ===== 페이지네이션 상태 ===== */
|
|
|
|
|
|
|
|
const pager = ref({ pageNum: 1, pageSize: 8, total: 0, pageLength: 1 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pagedProjects = computed(() => {
|
|
|
|
|
|
|
|
const total = projects.value.length;
|
|
|
|
|
|
|
|
pager.value.total = total;
|
|
|
|
|
|
|
|
pager.value.pageLength = Math.max(1, Math.ceil(total / pager.value.pageSize));
|
|
|
|
|
|
|
|
if (pager.value.pageNum > pager.value.pageLength)
|
|
|
|
|
|
|
|
pager.value.pageNum = pager.value.pageLength;
|
|
|
|
|
|
|
|
if (pager.value.pageNum < 1) pager.value.pageNum = 1;
|
|
|
|
|
|
|
|
const start = (pager.value.pageNum - 1) * pager.value.pageSize;
|
|
|
|
|
|
|
|
return projects.value.slice(start, start + pager.value.pageSize);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const changePageNum = (page: number) => {
|
|
|
|
|
|
|
|
pager.value.pageNum = page;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const changePageSize = (size: number) => {
|
|
|
|
|
|
|
|
pager.value.pageSize = size;
|
|
|
|
|
|
|
|
pager.value.pageNum = 1;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** ===== 서버 응답 타입 ===== */
|
|
|
|
/** ===== 서버 응답 타입 ===== */
|
|
|
|
interface ProjectSearchResponseItem {
|
|
|
|
interface ProjectSearchResponseItem {
|
|
|
|
id: number;
|
|
|
|
id: number;
|
|
|
|
prjNm: string;
|
|
|
|
prjNm: string;
|
|
|
|
prjDesc: string;
|
|
|
|
prjDesc: string;
|
|
|
|
prjStartDt?: string;
|
|
|
|
prjStartDt?: string;
|
|
|
|
regUserId?: string; // 화면의 "생성자"에 그대로 표시(콤마 구분 username들)
|
|
|
|
regUserId?: string;
|
|
|
|
|
|
|
|
regUserNm?: string;
|
|
|
|
|
|
|
|
modUserId?: string;
|
|
|
|
|
|
|
|
modUserNm?: string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
interface UserResponseItem {
|
|
|
|
interface UserResponseItem {
|
|
|
|
id: number | string;
|
|
|
|
id: number | string;
|
|
|
|
username: string;
|
|
|
|
username: string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** ===== 페이로드 타입(서비스 시그니처를 못 바꾼다는 가정에서 지역 타입 분리) ===== */
|
|
|
|
|
|
|
|
// 생성 시 mod* 안 보냄
|
|
|
|
|
|
|
|
type NewProjectPayload = {
|
|
|
|
|
|
|
|
id: null;
|
|
|
|
|
|
|
|
prjCd: string;
|
|
|
|
|
|
|
|
prjNm: string;
|
|
|
|
|
|
|
|
prjDesc: string;
|
|
|
|
|
|
|
|
prjStartDt: string;
|
|
|
|
|
|
|
|
prjEndDt: string;
|
|
|
|
|
|
|
|
delYn: string;
|
|
|
|
|
|
|
|
regDate: string;
|
|
|
|
|
|
|
|
regUserId?: string;
|
|
|
|
|
|
|
|
regUserNm?: string;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
// 수정 시 reg*는 기존 그대로 유지, mod*만 채움
|
|
|
|
|
|
|
|
type UpdateProjectPayload = {
|
|
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
|
|
prjCd: string;
|
|
|
|
|
|
|
|
prjNm: string;
|
|
|
|
|
|
|
|
prjDesc: string;
|
|
|
|
|
|
|
|
prjStartDt: string;
|
|
|
|
|
|
|
|
prjEndDt: string;
|
|
|
|
|
|
|
|
delYn: string;
|
|
|
|
|
|
|
|
regDate: string;
|
|
|
|
|
|
|
|
regUserId?: string;
|
|
|
|
|
|
|
|
regUserNm?: string;
|
|
|
|
|
|
|
|
modDate: string;
|
|
|
|
|
|
|
|
modUserId?: string;
|
|
|
|
|
|
|
|
modUserNm?: string;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** ===== 유틸 ===== */
|
|
|
|
/** ===== 유틸 ===== */
|
|
|
|
const buildApiProjectPayload = (): ApiProject => {
|
|
|
|
function buildCreatePayload(): NewProjectPayload {
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
|
|
const nowIso = new Date().toISOString();
|
|
|
|
const nowIso = new Date().toISOString();
|
|
|
|
const namesCsv = form.value.selectedUsers.join(",");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const names = form.value.selectedUsers;
|
|
|
|
|
|
|
|
const namesCsv = names.join(",");
|
|
|
|
|
|
|
|
const idsCsv = names
|
|
|
|
|
|
|
|
.map((name) => userOptions.value.find((u) => u.username === name)?.id)
|
|
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
|
|
(v): v is number | string => v !== undefined && v !== null && v !== "",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.map(String)
|
|
|
|
|
|
|
|
.join(","); // "6,5"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// mod* 없음 (생성)
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
id: modalMode.value === "edit" ? editingProjectId.value! : null,
|
|
|
|
id: null,
|
|
|
|
prjCd: form.value.prjCd,
|
|
|
|
prjCd: form.value.prjCd,
|
|
|
|
prjNm: form.value.prjNm,
|
|
|
|
prjNm: form.value.prjNm,
|
|
|
|
prjDesc: form.value.prjDesc,
|
|
|
|
prjDesc: form.value.prjDesc,
|
|
|
|
@ -80,13 +151,50 @@ const buildApiProjectPayload = (): ApiProject => {
|
|
|
|
prjEndDt: today,
|
|
|
|
prjEndDt: today,
|
|
|
|
delYn: "N",
|
|
|
|
delYn: "N",
|
|
|
|
regDate: nowIso,
|
|
|
|
regDate: nowIso,
|
|
|
|
regUserId: namesCsv,
|
|
|
|
regUserId: idsCsv,
|
|
|
|
regUserNm: namesCsv,
|
|
|
|
regUserNm: namesCsv,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildUpdatePayload(): UpdateProjectPayload {
|
|
|
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
|
|
|
|
|
|
const nowIso = new Date().toISOString();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const names = form.value.selectedUsers;
|
|
|
|
|
|
|
|
const namesCsv = names.join(",");
|
|
|
|
|
|
|
|
const idsCsv = names
|
|
|
|
|
|
|
|
.map((name) => userOptions.value.find((u) => u.username === name)?.id)
|
|
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
|
|
(v): v is number | string => v !== undefined && v !== null && v !== "",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.map(String)
|
|
|
|
|
|
|
|
.join(",");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const id = editingProjectId.value!;
|
|
|
|
|
|
|
|
const kept = projectRegById.value[id] || {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// reg*는 DB 원본 유지, mod*만 현재 선택으로 반영
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
id,
|
|
|
|
|
|
|
|
prjCd: form.value.prjCd,
|
|
|
|
|
|
|
|
prjNm: form.value.prjNm,
|
|
|
|
|
|
|
|
prjDesc: form.value.prjDesc,
|
|
|
|
|
|
|
|
prjStartDt: today,
|
|
|
|
|
|
|
|
prjEndDt: today,
|
|
|
|
|
|
|
|
delYn: "N",
|
|
|
|
|
|
|
|
regDate: nowIso,
|
|
|
|
|
|
|
|
regUserId: kept.regId,
|
|
|
|
|
|
|
|
regUserNm: kept.regNm,
|
|
|
|
modDate: nowIso,
|
|
|
|
modDate: nowIso,
|
|
|
|
modUserId: namesCsv,
|
|
|
|
modUserId: idsCsv,
|
|
|
|
modUserNm: namesCsv,
|
|
|
|
modUserNm: namesCsv,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fillerCount = computed(() =>
|
|
|
|
|
|
|
|
Math.max(0, pager.value.pageSize - pagedProjects.value.length),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
const fillers = computed(() => Array.from({ length: fillerCount.value }));
|
|
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
const resetForm = () => {
|
|
|
|
form.value.prjCd = `PRJ${Date.now()}`;
|
|
|
|
form.value.prjCd = `PRJ${Date.now()}`;
|
|
|
|
@ -99,13 +207,33 @@ const loadProjects = async () => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const { data } = await ProjectService.search();
|
|
|
|
const { data } = await ProjectService.search();
|
|
|
|
const rawList = data as ProjectSearchResponseItem[];
|
|
|
|
const rawList = data as ProjectSearchResponseItem[];
|
|
|
|
projects.value = rawList.map((p) => ({
|
|
|
|
|
|
|
|
|
|
|
|
const sorted = [...rawList].sort((a, b) => (b.id ?? 0) - (a.id ?? 0));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
projectRegById.value = {};
|
|
|
|
|
|
|
|
projects.value = sorted.map((p) => {
|
|
|
|
|
|
|
|
// 원본 reg 값은 따로 보관(수정 시 reg* 유지용)
|
|
|
|
|
|
|
|
projectRegById.value[p.id] = { regId: p.regUserId, regNm: p.regUserNm };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ★ 화면/모달에 보여줄 선택값은 mod_user_nm이 있으면 그걸로, 없으면 reg_user_nm
|
|
|
|
|
|
|
|
const displayNm =
|
|
|
|
|
|
|
|
p.modUserNm && p.modUserNm.length > 0 ? p.modUserNm : p.regUserNm || "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: p.id,
|
|
|
|
id: p.id,
|
|
|
|
title: p.prjNm,
|
|
|
|
title: p.prjNm,
|
|
|
|
creator: p.regUserId ?? "",
|
|
|
|
creator: displayNm, // ← 이 값이 모달 v-select v-model에 들어감
|
|
|
|
date: p.prjStartDt ?? "",
|
|
|
|
date: p.prjStartDt, // fallback 없이 그대로
|
|
|
|
description: p.prjDesc,
|
|
|
|
description: p.prjDesc,
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
|
|
pager.value.pageNum >
|
|
|
|
|
|
|
|
Math.max(1, Math.ceil(projects.value.length / pager.value.pageSize))
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
pager.value.pageNum = 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
console.error("프로젝트 조회 실패:", e);
|
|
|
|
console.error("프로젝트 조회 실패:", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -136,30 +264,27 @@ const closeDialog = () => {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectProject = (index: number) => {
|
|
|
|
const selectProject = (index: number) => {
|
|
|
|
const selected = projects.value[index];
|
|
|
|
const selected = pagedProjects.value[index];
|
|
|
|
autoflowStore.setProjectId(selected.id);
|
|
|
|
autoflowStore.setProjectId(selected.id);
|
|
|
|
autoflowStore.setProjectName(selected.title);
|
|
|
|
autoflowStore.setProjectName(selected.title);
|
|
|
|
router.push("/home");
|
|
|
|
router.push("/home");
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** ===== 프로젝트 저장 & 권한 부여 ===== */
|
|
|
|
|
|
|
|
const grantDefaultPermissions = async (
|
|
|
|
const grantDefaultPermissions = async (
|
|
|
|
projectId: number,
|
|
|
|
projectId: number,
|
|
|
|
usernames: string[],
|
|
|
|
usernames: string[],
|
|
|
|
) => {
|
|
|
|
) => {
|
|
|
|
if (!usernames?.length) return;
|
|
|
|
if (!usernames?.length) return;
|
|
|
|
|
|
|
|
|
|
|
|
const nameSet = new Set(usernames);
|
|
|
|
const nameSet = new Set(usernames);
|
|
|
|
const numericIds = userOptions.value
|
|
|
|
const numericIds = userOptions.value
|
|
|
|
.filter((u) => nameSet.has(u.username))
|
|
|
|
.filter((u) => nameSet.has(u.username))
|
|
|
|
.map((u) => Number(u.id))
|
|
|
|
.map((u) => Number(u.id))
|
|
|
|
.filter((n) => Number.isFinite(n));
|
|
|
|
.filter((n) => Number.isFinite(n));
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
await Promise.all(
|
|
|
|
numericIds.map((uid) =>
|
|
|
|
numericIds.map((uid) =>
|
|
|
|
ProjectService.projectAuthority(projectId, {
|
|
|
|
ProjectService.projectAuthority(projectId, {
|
|
|
|
projectId,
|
|
|
|
projectId,
|
|
|
|
userId: uid, // number로 보장
|
|
|
|
userId: uid,
|
|
|
|
permissions: DEFAULT_PERMISSIONS,
|
|
|
|
permissions: DEFAULT_PERMISSIONS,
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
@ -171,20 +296,17 @@ const saveProject = async () => {
|
|
|
|
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
|
|
|
|
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const payload = buildApiProjectPayload();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
let projectId: number;
|
|
|
|
let projectId: number;
|
|
|
|
|
|
|
|
|
|
|
|
if (modalMode.value === "create") {
|
|
|
|
if (modalMode.value === "create") {
|
|
|
|
const createRes = await ProjectService.add(payload);
|
|
|
|
const createPayload = buildCreatePayload(); // mod* 없음
|
|
|
|
|
|
|
|
const createRes = await ProjectService.add(createPayload); // ← 오타 수정
|
|
|
|
projectId = createRes.data.id;
|
|
|
|
projectId = createRes.data.id;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
await ProjectService.update(editingProjectId.value!, payload);
|
|
|
|
const updatePayload = buildUpdatePayload(); // reg* 유지, mod* 반영
|
|
|
|
|
|
|
|
await ProjectService.update(editingProjectId.value!, updatePayload); // ← non-null 보장
|
|
|
|
projectId = editingProjectId.value!;
|
|
|
|
projectId = editingProjectId.value!;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await grantDefaultPermissions(projectId, form.value.selectedUsers);
|
|
|
|
await grantDefaultPermissions(projectId, form.value.selectedUsers);
|
|
|
|
await loadProjects();
|
|
|
|
await loadProjects();
|
|
|
|
closeDialog();
|
|
|
|
closeDialog();
|
|
|
|
@ -197,8 +319,7 @@ const saveProject = async () => {
|
|
|
|
const deleteProject = async () => {
|
|
|
|
const deleteProject = async () => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (selectedIndex.value === null) return;
|
|
|
|
if (selectedIndex.value === null) return;
|
|
|
|
const target = projects.value[selectedIndex.value];
|
|
|
|
const target = pagedProjects.value[selectedIndex.value];
|
|
|
|
|
|
|
|
|
|
|
|
await ProjectService.delete(target.id);
|
|
|
|
await ProjectService.delete(target.id);
|
|
|
|
await loadProjects();
|
|
|
|
await loadProjects();
|
|
|
|
} catch (e: any) {
|
|
|
|
} catch (e: any) {
|
|
|
|
@ -209,43 +330,6 @@ const deleteProject = async () => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 프로젝트 권한있을 때
|
|
|
|
|
|
|
|
// const deleteProject = async (): Promise<void> => {
|
|
|
|
|
|
|
|
// try {
|
|
|
|
|
|
|
|
// if (selectedIndex.value === null) return;
|
|
|
|
|
|
|
|
// const target = projects.value[selectedIndex.value];
|
|
|
|
|
|
|
|
// const projectId = target.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // 1) 프로젝트에 연결된 username들 뽑기
|
|
|
|
|
|
|
|
// const usernames = (target.creator || "")
|
|
|
|
|
|
|
|
// .split(",")
|
|
|
|
|
|
|
|
// .map((s) => s.trim())
|
|
|
|
|
|
|
|
// .filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // 2) username -> userId 매핑
|
|
|
|
|
|
|
|
// if (usernames.length) {
|
|
|
|
|
|
|
|
// const ids = userOptions.value
|
|
|
|
|
|
|
|
// .filter((u) => usernames.includes(u.username))
|
|
|
|
|
|
|
|
// .map((u) => u.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // 3) 각 사용자 권한/매핑 제거
|
|
|
|
|
|
|
|
// await Promise.all(
|
|
|
|
|
|
|
|
// ids.map((uid) => ProjectService.deleteProjectAuthority(projectId, uid)),
|
|
|
|
|
|
|
|
// );
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // 4) 마지막에 프로젝트 삭제
|
|
|
|
|
|
|
|
// await ProjectService.delete(projectId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// await loadProjects();
|
|
|
|
|
|
|
|
// } catch (e: any) {
|
|
|
|
|
|
|
|
// console.error("삭제 실패:", e?.response?.status, e?.response?.data || e);
|
|
|
|
|
|
|
|
// alert("삭제 실패: " + (e?.response?.data?.message || e.message || ""));
|
|
|
|
|
|
|
|
// } finally {
|
|
|
|
|
|
|
|
// contextMenu.value = false;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onAddProject = () => {
|
|
|
|
const onAddProject = () => {
|
|
|
|
if (!isAdmin.value) {
|
|
|
|
if (!isAdmin.value) {
|
|
|
|
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
|
|
|
|
alert("권한이 없습니다. (ROLE_ADMIN 전용)");
|
|
|
|
@ -260,19 +344,19 @@ const onAddProject = () => {
|
|
|
|
const modifyProject = () => {
|
|
|
|
const modifyProject = () => {
|
|
|
|
contextMenu.value = false;
|
|
|
|
contextMenu.value = false;
|
|
|
|
if (selectedIndex.value === null) return;
|
|
|
|
if (selectedIndex.value === null) return;
|
|
|
|
|
|
|
|
const selected = pagedProjects.value[selectedIndex.value];
|
|
|
|
|
|
|
|
|
|
|
|
const selected = projects.value[selectedIndex.value];
|
|
|
|
|
|
|
|
modalMode.value = "edit";
|
|
|
|
modalMode.value = "edit";
|
|
|
|
editingProjectId.value = selected.id;
|
|
|
|
editingProjectId.value = selected.id;
|
|
|
|
|
|
|
|
|
|
|
|
form.value.prjCd = selected.title;
|
|
|
|
form.value.prjCd = selected.title;
|
|
|
|
form.value.prjNm = selected.title;
|
|
|
|
form.value.prjNm = selected.title;
|
|
|
|
form.value.prjDesc = selected.description;
|
|
|
|
form.value.prjDesc = selected.description;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// creator는 reg_user_nm 그대로 들어오므로 그대로 분해. 값 없으면 []
|
|
|
|
form.value.selectedUsers =
|
|
|
|
form.value.selectedUsers =
|
|
|
|
selected.creator
|
|
|
|
typeof selected.creator === "string" && selected.creator.length
|
|
|
|
?.split(",")
|
|
|
|
? selected.creator.split(",").map((s) => s.trim())
|
|
|
|
.map((s) => s.trim())
|
|
|
|
: [];
|
|
|
|
.filter(Boolean) ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dialog.value = true;
|
|
|
|
dialog.value = true;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -301,7 +385,6 @@ onBeforeUnmount(() => {
|
|
|
|
</v-col>
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
|
|
<v-col cols="auto">
|
|
|
|
<v-col cols="auto">
|
|
|
|
<!-- ADMIN만 노출 -->
|
|
|
|
|
|
|
|
<v-btn
|
|
|
|
<v-btn
|
|
|
|
v-show="isAdmin"
|
|
|
|
v-show="isAdmin"
|
|
|
|
color="secondary"
|
|
|
|
color="secondary"
|
|
|
|
@ -315,10 +398,13 @@ onBeforeUnmount(() => {
|
|
|
|
</v-col>
|
|
|
|
</v-col>
|
|
|
|
</v-row>
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 카드 그리드: 현재 페이지 데이터만 렌더 -->
|
|
|
|
|
|
|
|
|
|
|
|
<v-row dense>
|
|
|
|
<v-row dense>
|
|
|
|
|
|
|
|
<!-- 실제 카드 -->
|
|
|
|
<v-col
|
|
|
|
<v-col
|
|
|
|
v-for="(project, index) in projects"
|
|
|
|
v-for="(project, index) in pagedProjects"
|
|
|
|
:key="index"
|
|
|
|
:key="project.id"
|
|
|
|
cols="12"
|
|
|
|
cols="12"
|
|
|
|
sm="6"
|
|
|
|
sm="6"
|
|
|
|
md="6"
|
|
|
|
md="6"
|
|
|
|
@ -326,7 +412,7 @@ onBeforeUnmount(() => {
|
|
|
|
class="d-flex"
|
|
|
|
class="d-flex"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<v-card
|
|
|
|
<v-card
|
|
|
|
class="pa-4 flex-grow-1 d-flex flex-column"
|
|
|
|
class="project-card pa-4 flex-grow-1 d-flex flex-column"
|
|
|
|
color="primary"
|
|
|
|
color="primary"
|
|
|
|
variant="elevated"
|
|
|
|
variant="elevated"
|
|
|
|
elevation="6"
|
|
|
|
elevation="6"
|
|
|
|
@ -354,8 +440,46 @@ onBeforeUnmount(() => {
|
|
|
|
</v-card-text>
|
|
|
|
</v-card-text>
|
|
|
|
</v-card>
|
|
|
|
</v-card>
|
|
|
|
</v-col>
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 플레이스홀더(보이지 않지만 자리 차지) -->
|
|
|
|
|
|
|
|
<v-col
|
|
|
|
|
|
|
|
v-for="(_, i) in fillers"
|
|
|
|
|
|
|
|
:key="'filler-' + i"
|
|
|
|
|
|
|
|
cols="12"
|
|
|
|
|
|
|
|
sm="6"
|
|
|
|
|
|
|
|
md="6"
|
|
|
|
|
|
|
|
lg="6"
|
|
|
|
|
|
|
|
class="d-flex"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<v-card
|
|
|
|
|
|
|
|
class="project-card pa-4 flex-grow-1"
|
|
|
|
|
|
|
|
style="visibility: hidden; pointer-events: none"
|
|
|
|
|
|
|
|
rounded="lg"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
|
|
</v-col>
|
|
|
|
</v-row>
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 하단 페이지네이션 영역 -->
|
|
|
|
|
|
|
|
<v-row class="mt-6" align="center" justify="center">
|
|
|
|
|
|
|
|
<v-col
|
|
|
|
|
|
|
|
cols="12"
|
|
|
|
|
|
|
|
class="d-flex justify-center align-center"
|
|
|
|
|
|
|
|
style="gap: 16px"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<v-pagination
|
|
|
|
|
|
|
|
v-model="pager.pageNum"
|
|
|
|
|
|
|
|
:length="pager.pageLength"
|
|
|
|
|
|
|
|
:total-visible="7"
|
|
|
|
|
|
|
|
color="primary"
|
|
|
|
|
|
|
|
rounded="circle"
|
|
|
|
|
|
|
|
@update:model-value="changePageNum"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
</v-row>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 컨텍스트 메뉴/다이얼로그 등 기존 그대로 -->
|
|
|
|
<v-menu
|
|
|
|
<v-menu
|
|
|
|
v-model="contextMenu"
|
|
|
|
v-model="contextMenu"
|
|
|
|
absolute
|
|
|
|
absolute
|
|
|
|
@ -407,14 +531,18 @@ onBeforeUnmount(() => {
|
|
|
|
|
|
|
|
|
|
|
|
<v-card-actions>
|
|
|
|
<v-card-actions>
|
|
|
|
<v-spacer />
|
|
|
|
<v-spacer />
|
|
|
|
<v-btn text @click="closeDialog">Cancel</v-btn>
|
|
|
|
|
|
|
|
<v-btn color="primary" @click="saveProject">
|
|
|
|
<v-btn color="primary" @click="saveProject">
|
|
|
|
{{ modalMode === "create" ? "Create" : "Save" }}
|
|
|
|
{{ modalMode === "create" ? "Create" : "Save" }}
|
|
|
|
</v-btn>
|
|
|
|
</v-btn>
|
|
|
|
|
|
|
|
<v-btn text @click="closeDialog">Cancel</v-btn>
|
|
|
|
</v-card-actions>
|
|
|
|
</v-card-actions>
|
|
|
|
</v-card>
|
|
|
|
</v-card>
|
|
|
|
</v-dialog>
|
|
|
|
</v-dialog>
|
|
|
|
</v-container>
|
|
|
|
</v-container>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped></style>
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
.project-card {
|
|
|
|
|
|
|
|
min-height: 140px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|