diff --git a/.run/dev.run.xml b/.run/dev.run.xml new file mode 100644 index 0000000..4c69008 --- /dev/null +++ b/.run/dev.run.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/api/feedback/[id]/delete.delete.ts b/backend/api/feedback/[id]/delete.delete.ts new file mode 100644 index 0000000..3320d4d --- /dev/null +++ b/backend/api/feedback/[id]/delete.delete.ts @@ -0,0 +1,39 @@ +import { query, execute } from '../../../utils/db' + +/** + * 개선의견 삭제 + * DELETE /api/feedback/[id]/delete + */ +export default defineEventHandler(async (event) => { + const userId = getCookie(event, 'user_id') + if (!userId) { + throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) + } + + const feedbackId = getRouterParam(event, 'id') + if (!feedbackId) { + throw createError({ statusCode: 400, message: '피드백 ID가 필요합니다.' }) + } + + // 본인 확인 + const feedback = await query(` + SELECT author_id FROM wr_feedback WHERE feedback_id = $1 + `, [feedbackId]) + + if (!feedback[0]) { + throw createError({ statusCode: 404, message: '의견을 찾을 수 없습니다.' }) + } + + if (feedback[0].author_id !== parseInt(userId)) { + throw createError({ statusCode: 403, message: '본인의 의견만 삭제할 수 있습니다.' }) + } + + // 공감 먼저 삭제 (CASCADE로 자동 삭제되지만 명시적으로) + await execute(`DELETE FROM wr_feedback_like WHERE feedback_id = $1`, [feedbackId]) + await execute(`DELETE FROM wr_feedback WHERE feedback_id = $1`, [feedbackId]) + + return { + success: true, + message: '삭제되었습니다.' + } +}) diff --git a/backend/api/feedback/[id]/like.post.ts b/backend/api/feedback/[id]/like.post.ts new file mode 100644 index 0000000..2416693 --- /dev/null +++ b/backend/api/feedback/[id]/like.post.ts @@ -0,0 +1,66 @@ +import { query, execute, queryOne } from '../../../utils/db' + +/** + * 개선의견 공감 토글 + * POST /api/feedback/[id]/like + */ +export default defineEventHandler(async (event) => { + const userId = getCookie(event, 'user_id') + if (!userId) { + throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) + } + + const feedbackId = getRouterParam(event, 'id') + if (!feedbackId) { + throw createError({ statusCode: 400, message: '피드백 ID가 필요합니다.' }) + } + + // 피드백 존재 확인 + const feedback = await query(` + SELECT feedback_id FROM wr_feedback WHERE feedback_id = $1 + `, [feedbackId]) + + if (!feedback[0]) { + throw createError({ statusCode: 404, message: '의견을 찾을 수 없습니다.' }) + } + + // 이미 공감했는지 확인 + const existing = await query(` + SELECT 1 FROM wr_feedback_like WHERE feedback_id = $1 AND employee_id = $2 + `, [feedbackId, userId]) + + let isLiked: boolean + let likeCount: number + + if (existing[0]) { + // 공감 취소 + await execute(` + DELETE FROM wr_feedback_like WHERE feedback_id = $1 AND employee_id = $2 + `, [feedbackId, userId]) + await execute(` + UPDATE wr_feedback SET like_count = like_count - 1 WHERE feedback_id = $1 + `, [feedbackId]) + isLiked = false + } else { + // 공감 추가 + await execute(` + INSERT INTO wr_feedback_like (feedback_id, employee_id) VALUES ($1, $2) + `, [feedbackId, userId]) + await execute(` + UPDATE wr_feedback SET like_count = like_count + 1 WHERE feedback_id = $1 + `, [feedbackId]) + isLiked = true + } + + // 최신 카운트 조회 + const updated = await queryOne(` + SELECT like_count FROM wr_feedback WHERE feedback_id = $1 + `, [feedbackId]) + likeCount = updated.like_count + + return { + success: true, + isLiked, + likeCount + } +}) diff --git a/backend/api/feedback/[id]/update.put.ts b/backend/api/feedback/[id]/update.put.ts new file mode 100644 index 0000000..f7fee2a --- /dev/null +++ b/backend/api/feedback/[id]/update.put.ts @@ -0,0 +1,57 @@ +import { query, execute } from '../../../utils/db' + +/** + * 개선의견 수정 + * PUT /api/feedback/[id]/update + */ +export default defineEventHandler(async (event) => { + const userId = getCookie(event, 'user_id') + if (!userId) { + throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) + } + + const feedbackId = getRouterParam(event, 'id') + if (!feedbackId) { + throw createError({ statusCode: 400, message: '피드백 ID가 필요합니다.' }) + } + + // 본인 확인 + const feedback = await query(` + SELECT author_id FROM wr_feedback WHERE feedback_id = $1 + `, [feedbackId]) + + if (!feedback[0]) { + throw createError({ statusCode: 404, message: '의견을 찾을 수 없습니다.' }) + } + + if (feedback[0].author_id !== parseInt(userId)) { + throw createError({ statusCode: 403, message: '본인의 의견만 수정할 수 있습니다.' }) + } + + const body = await readBody<{ + category?: string + content?: string + }>(event) + + if (!body.content?.trim()) { + throw createError({ statusCode: 400, message: '내용을 입력해주세요.' }) + } + + const validCategories = ['FEATURE', 'UI', 'BUG', 'ETC'] + if (body.category && !validCategories.includes(body.category)) { + throw createError({ statusCode: 400, message: '올바른 카테고리를 선택해주세요.' }) + } + + await execute(` + UPDATE wr_feedback + SET category = COALESCE($1, category), + content = $2, + updated_at = NOW() + WHERE feedback_id = $3 + `, [body.category, body.content.trim(), feedbackId]) + + return { + success: true, + message: '수정되었습니다.' + } +}) diff --git a/backend/api/feedback/create.post.ts b/backend/api/feedback/create.post.ts new file mode 100644 index 0000000..787a571 --- /dev/null +++ b/backend/api/feedback/create.post.ts @@ -0,0 +1,38 @@ +import { query, queryOne } from '../../utils/db' + +/** + * 개선의견 작성 + * POST /api/feedback/create + */ +export default defineEventHandler(async (event) => { + const userId = getCookie(event, 'user_id') + if (!userId) { + throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) + } + + const body = await readBody<{ + category: string + content: string + }>(event) + + if (!body.category || !body.content?.trim()) { + throw createError({ statusCode: 400, message: '카테고리와 내용을 입력해주세요.' }) + } + + const validCategories = ['FEATURE', 'UI', 'BUG', 'ETC'] + if (!validCategories.includes(body.category)) { + throw createError({ statusCode: 400, message: '올바른 카테고리를 선택해주세요.' }) + } + + const result = await queryOne(` + INSERT INTO wr_feedback (author_id, category, content) + VALUES ($1, $2, $3) + RETURNING feedback_id + `, [userId, body.category, body.content.trim()]) + + return { + success: true, + feedbackId: result.feedback_id, + message: '의견이 등록되었습니다.' + } +}) diff --git a/backend/api/feedback/list.get.ts b/backend/api/feedback/list.get.ts new file mode 100644 index 0000000..2b68b2d --- /dev/null +++ b/backend/api/feedback/list.get.ts @@ -0,0 +1,103 @@ +import { query } from '../../utils/db' + +/** + * 개선의견 목록 조회 + * GET /api/feedback/list + */ +export default defineEventHandler(async (event) => { + const userId = getCookie(event, 'user_id') + if (!userId) { + throw createError({ statusCode: 401, message: '로그인이 필요합니다.' }) + } + + const q = getQuery(event) + const page = parseInt(q.page as string) || 1 + const limit = parseInt(q.limit as string) || 12 + const category = q.category as string || '' + const offset = (page - 1) * limit + + // 조건 구성 + let whereClause = '' + const countParams: any[] = [] + + if (category) { + whereClause = 'WHERE f.category = $1' + countParams.push(category) + } + + // 전체 개수 + const countResult = await query(` + SELECT COUNT(*) as total FROM wr_feedback f ${whereClause} + `, countParams) + const total = parseInt(countResult[0].total) + + // 목록 조회 + let feedbacks: any[] + + if (category) { + feedbacks = await query(` + SELECT + f.feedback_id, + f.author_id, + e.employee_name as author_name, + f.category, + f.content, + f.like_count, + f.is_resolved, + f.created_at, + f.updated_at, + EXISTS( + SELECT 1 FROM wr_feedback_like fl + WHERE fl.feedback_id = f.feedback_id AND fl.employee_id = $1 + ) as is_liked + FROM wr_feedback f + JOIN wr_employee_info e ON f.author_id = e.employee_id + WHERE f.category = $2 + ORDER BY f.created_at DESC + LIMIT $3 OFFSET $4 + `, [userId, category, limit, offset]) + } else { + feedbacks = await query(` + SELECT + f.feedback_id, + f.author_id, + e.employee_name as author_name, + f.category, + f.content, + f.like_count, + f.is_resolved, + f.created_at, + f.updated_at, + EXISTS( + SELECT 1 FROM wr_feedback_like fl + WHERE fl.feedback_id = f.feedback_id AND fl.employee_id = $1 + ) as is_liked + FROM wr_feedback f + JOIN wr_employee_info e ON f.author_id = e.employee_id + ORDER BY f.created_at DESC + LIMIT $2 OFFSET $3 + `, [userId, limit, offset]) + } + + return { + feedbacks: feedbacks.map((f: any) => ({ + feedbackId: f.feedback_id, + authorId: f.author_id, + authorName: f.author_name, + category: f.category, + content: f.content, + likeCount: f.like_count, + isResolved: f.is_resolved, + createdAt: f.created_at, + updatedAt: f.updated_at, + isLiked: f.is_liked, + isOwner: f.author_id === parseInt(userId) + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + } +}) diff --git a/frontend/components/layout/AppHeader.vue b/frontend/components/layout/AppHeader.vue index 634d281..9cfe355 100644 --- a/frontend/components/layout/AppHeader.vue +++ b/frontend/components/layout/AppHeader.vue @@ -37,6 +37,11 @@ 직원관리 + + + 개선의견 + + diff --git a/frontend/feedback/index.vue b/frontend/feedback/index.vue new file mode 100644 index 0000000..e18449e --- /dev/null +++ b/frontend/feedback/index.vue @@ -0,0 +1,405 @@ + + + + + + + + + 개선의견 + 파일럿 프로젝트 피드백을 남겨주세요 + + + 의견 작성 + + + + + + + 전체 + + {{ cat.icon }} {{ cat.label }} + + + + + + + + + + + + + {{ getCategoryIcon(fb.category) }} {{ getCategoryLabel(fb.category) }} + + + + + + + + 수정 + + + 삭제 + + + + + + + {{ fb.content }} + + + + + + + + + + 아직 등록된 의견이 없습니다. + + 첫 번째 의견 남기기 + + + + + + + + + + + + + 이전 + + + {{ p }} + + + 다음 + + + + + + + + + + + + {{ isEdit ? '의견 수정' : '의견 작성' }} + + + + + + 카테고리 * + + + {{ cat.icon }} {{ cat.label }} + + + + + 내용 * + + {{ form.content.length }}/500 + + + + + + + + + + + + + + 삭제 확인 + + + + 이 의견을 삭제하시겠습니까? + + + + + + + + + + + + diff --git a/package.json b/package.json index 65547bd..5c6bf69 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "주간업무보고 시스템", "private": true, "scripts": { - "dev": "nuxt dev", + "dev": "nuxt dev --port 2026", "build": "nuxt build", "generate": "nuxt generate", "preview": "nuxt preview",
파일럿 프로젝트 피드백을 남겨주세요
{{ fb.content }}
아직 등록된 의견이 없습니다.