diff --git a/backend-node/src/controllers/systemNoticeController.ts b/backend-node/src/controllers/systemNoticeController.ts new file mode 100644 index 00000000..9a00a4f0 --- /dev/null +++ b/backend-node/src/controllers/systemNoticeController.ts @@ -0,0 +1,275 @@ +import { Response } from "express"; +import { AuthenticatedRequest } from "../types/auth"; +import { logger } from "../utils/logger"; +import { query } from "../database/db"; + +/** + * GET /api/system-notices + * 공지사항 목록 조회 + * - 최고 관리자(*): 전체 조회 + * - 일반 회사: 자신의 company_code 데이터만 조회 + * - is_active 필터 옵션 지원 + */ +export const getSystemNotices = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const companyCode = req.user!.companyCode; + const { is_active } = req.query; + + logger.info("공지사항 목록 조회 요청", { companyCode, is_active }); + + const conditions: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + // 최고 관리자가 아닌 경우 company_code 필터링 + if (companyCode !== "*") { + conditions.push(`company_code = $${paramIndex}`); + params.push(companyCode); + paramIndex++; + } + + // is_active 필터 (true/false 문자열 처리) + if (is_active !== undefined && is_active !== "") { + const activeValue = is_active === "true" || is_active === "1"; + conditions.push(`is_active = $${paramIndex}`); + params.push(activeValue); + paramIndex++; + } + + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + + const rows = await query( + `SELECT + id, + company_code, + title, + content, + is_active, + created_by, + created_at, + updated_at + FROM system_notice + ${whereClause} + ORDER BY created_at DESC`, + params + ); + + logger.info("공지사항 목록 조회 성공", { + companyCode, + count: rows.length, + }); + + res.status(200).json({ + success: true, + data: rows, + total: rows.length, + }); + } catch (error) { + logger.error("공지사항 목록 조회 실패", { error }); + res.status(500).json({ + success: false, + message: "공지사항 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}; + +/** + * POST /api/system-notices + * 공지사항 등록 + * - company_code는 req.user.companyCode에서 자동 추출 (클라이언트 입력 신뢰 금지) + */ +export const createSystemNotice = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { title, content, is_active = true } = req.body; + + logger.info("공지사항 등록 요청", { companyCode, userId, title }); + + if (!title || !title.trim()) { + res.status(400).json({ + success: false, + message: "제목을 입력해주세요.", + }); + return; + } + + if (!content || !content.trim()) { + res.status(400).json({ + success: false, + message: "내용을 입력해주세요.", + }); + return; + } + + const [created] = await query( + `INSERT INTO system_notice (company_code, title, content, is_active, created_by) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [companyCode, title.trim(), content.trim(), is_active, userId] + ); + + logger.info("공지사항 등록 성공", { + id: created.id, + companyCode, + title: created.title, + }); + + res.status(201).json({ + success: true, + data: created, + message: "공지사항이 등록되었습니다.", + }); + } catch (error) { + logger.error("공지사항 등록 실패", { error }); + res.status(500).json({ + success: false, + message: "공지사항 등록 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}; + +/** + * PUT /api/system-notices/:id + * 공지사항 수정 + * - WHERE id=$1 AND company_code=$2 로 타 회사 데이터 수정 차단 + * - 최고 관리자는 company_code 조건 없이 id만으로 수정 가능 + */ +export const updateSystemNotice = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const companyCode = req.user!.companyCode; + const { id } = req.params; + const { title, content, is_active } = req.body; + + logger.info("공지사항 수정 요청", { id, companyCode }); + + if (!title || !title.trim()) { + res.status(400).json({ + success: false, + message: "제목을 입력해주세요.", + }); + return; + } + + if (!content || !content.trim()) { + res.status(400).json({ + success: false, + message: "내용을 입력해주세요.", + }); + return; + } + + let result: any[]; + + if (companyCode === "*") { + // 최고 관리자: id만으로 수정 + result = await query( + `UPDATE system_notice + SET title = $1, content = $2, is_active = $3, updated_at = NOW() + WHERE id = $4 + RETURNING *`, + [title.trim(), content.trim(), is_active ?? true, id] + ); + } else { + // 일반 회사: company_code 추가 조건으로 타 회사 데이터 수정 차단 + result = await query( + `UPDATE system_notice + SET title = $1, content = $2, is_active = $3, updated_at = NOW() + WHERE id = $4 AND company_code = $5 + RETURNING *`, + [title.trim(), content.trim(), is_active ?? true, id, companyCode] + ); + } + + if (!result || result.length === 0) { + res.status(404).json({ + success: false, + message: "공지사항을 찾을 수 없거나 수정 권한이 없습니다.", + }); + return; + } + + logger.info("공지사항 수정 성공", { id, companyCode }); + + res.status(200).json({ + success: true, + data: result[0], + message: "공지사항이 수정되었습니다.", + }); + } catch (error) { + logger.error("공지사항 수정 실패", { error, id: req.params.id }); + res.status(500).json({ + success: false, + message: "공지사항 수정 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}; + +/** + * DELETE /api/system-notices/:id + * 공지사항 삭제 + * - WHERE id=$1 AND company_code=$2 로 타 회사 데이터 삭제 차단 + * - 최고 관리자는 company_code 조건 없이 id만으로 삭제 가능 + */ +export const deleteSystemNotice = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const companyCode = req.user!.companyCode; + const { id } = req.params; + + logger.info("공지사항 삭제 요청", { id, companyCode }); + + let result: any[]; + + if (companyCode === "*") { + // 최고 관리자: id만으로 삭제 + result = await query( + `DELETE FROM system_notice WHERE id = $1 RETURNING id`, + [id] + ); + } else { + // 일반 회사: company_code 추가 조건으로 타 회사 데이터 삭제 차단 + result = await query( + `DELETE FROM system_notice WHERE id = $1 AND company_code = $2 RETURNING id`, + [id, companyCode] + ); + } + + if (!result || result.length === 0) { + res.status(404).json({ + success: false, + message: "공지사항을 찾을 수 없거나 삭제 권한이 없습니다.", + }); + return; + } + + logger.info("공지사항 삭제 성공", { id, companyCode }); + + res.status(200).json({ + success: true, + message: "공지사항이 삭제되었습니다.", + }); + } catch (error) { + logger.error("공지사항 삭제 실패", { error, id: req.params.id }); + res.status(500).json({ + success: false, + message: "공지사항 삭제 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}; diff --git a/backend-node/src/routes/systemNoticeRoutes.ts b/backend-node/src/routes/systemNoticeRoutes.ts new file mode 100644 index 00000000..54506386 --- /dev/null +++ b/backend-node/src/routes/systemNoticeRoutes.ts @@ -0,0 +1,27 @@ +import { Router } from "express"; +import { + getSystemNotices, + createSystemNotice, + updateSystemNotice, + deleteSystemNotice, +} from "../controllers/systemNoticeController"; +import { authenticateToken } from "../middleware/authMiddleware"; + +const router = Router(); + +// 모든 공지사항 라우트에 인증 미들웨어 적용 +router.use(authenticateToken); + +// 공지사항 목록 조회 (is_active 필터 쿼리 파라미터 지원) +router.get("/", getSystemNotices); + +// 공지사항 등록 +router.post("/", createSystemNotice); + +// 공지사항 수정 +router.put("/:id", updateSystemNotice); + +// 공지사항 삭제 +router.delete("/:id", deleteSystemNotice); + +export default router;