From 969efe3fc33fbcd9c2389f623a9a569d5c89b35a Mon Sep 17 00:00:00 2001 From: pray4skylark Date: Mon, 8 Jun 2026 18:01:53 +0900 Subject: [PATCH] feat: back button + reopen for approved/rejected tickets --- .../srsystem/controller/TicketController.java | 7 +++ .../com/srsystem/service/TicketService.java | 13 ++++++ src/app/(main)/issues/[id]/page.tsx | 46 +++++++++++++++++-- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/srsystem/controller/TicketController.java b/backend/src/main/java/com/srsystem/controller/TicketController.java index 98d4cd8..03780ca 100644 --- a/backend/src/main/java/com/srsystem/controller/TicketController.java +++ b/backend/src/main/java/com/srsystem/controller/TicketController.java @@ -135,6 +135,13 @@ public class TicketController { return ResponseEntity.ok(ApiResponse.ok("반려되었습니다", ticketService.rejectTicket(ticketId, currentUser.getEmpId(), body.get("reason")))); } + @Operation(summary = "티켓 재오픈", description = "APPROVED/REJECTED 상태를 PENDING_APPROVAL로 되돌립니다") + @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") + @PostMapping("/{ticketId}/reopen") + public ResponseEntity> reopenTicket(@PathVariable Long ticketId) { + return ResponseEntity.ok(ApiResponse.ok("재오픈되었습니다", ticketService.reopenTicket(ticketId))); + } + @Operation(summary = "댓글 목록 조회", description = "티켓의 댓글 목록을 조회합니다") @PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')") @GetMapping("/{ticketId}/comments") diff --git a/backend/src/main/java/com/srsystem/service/TicketService.java b/backend/src/main/java/com/srsystem/service/TicketService.java index 3cfc915..5277915 100644 --- a/backend/src/main/java/com/srsystem/service/TicketService.java +++ b/backend/src/main/java/com/srsystem/service/TicketService.java @@ -234,6 +234,19 @@ public class TicketService { return toIssueTicket(ticketRepository.save(ticket)); } + @Transactional + public IssueTicket reopenTicket(Long ticketId) { + Ticket ticket = ticketRepository.findById(ticketId) + .orElseThrow(() -> new BusinessException(ErrorCode.TICKET_NOT_FOUND)); + if (ticket.getStatus() != TicketStatus.APPROVED && ticket.getStatus() != TicketStatus.REJECTED) { + throw new BusinessException(ErrorCode.INVALID_STATUS); + } + ticket.setStatus(TicketStatus.PENDING_APPROVAL); + ticket.setApprovedBy(null); + ticket.setApprovedAt(null); + return toIssueTicket(ticketRepository.save(ticket)); + } + private IssueTicket toIssueTicket(Ticket ticket) { return new IssueTicket( ticket.getTicketId(), diff --git a/src/app/(main)/issues/[id]/page.tsx b/src/app/(main)/issues/[id]/page.tsx index 6f4b19c..372b408 100644 --- a/src/app/(main)/issues/[id]/page.tsx +++ b/src/app/(main)/issues/[id]/page.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { ChevronLeft, Clock, User, MessageSquare, Paperclip, Send, CheckCircle2, ArrowRight } from 'lucide-react'; +import { ChevronLeft, Clock, User, MessageSquare, Paperclip, Send, CheckCircle2, ArrowRight, RotateCcw } from 'lucide-react'; import Link from 'next/link'; import ticketService from '@/services/ticketService'; import apiClient from '@/services/apiClient'; @@ -72,7 +72,10 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri const currentIdx = statusFlow.indexOf(status); const canAdvance = currentIdx < statusFlow.length - 1; + const canGoBack = currentIdx > 0; const isClosed = status === 'CLOSED'; + const isApprovalTicket = ['VACATION', 'EXPENSE', 'EQUIPMENT_REQUEST', 'GENERAL'].includes(ticket?.category || ''); + const isMgmt = user?.roleCode !== 'EMPLOYEE'; const handleAdvance = async () => { if (!canAdvance) return; @@ -85,6 +88,24 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri } }; + const handleBack = async () => { + if (!canGoBack) return; + const prevStatus = statusFlow[currentIdx - 1]; + try { + await ticketService.updateStatus(ticketId, prevStatus); + setStatus(prevStatus); + } catch (err: unknown) { + alert((err as { message?: string })?.message || '상태 변경 실패'); + } + }; + + const handleReopen = async () => { + try { + const res: any = await apiClient.post(`/tickets/${ticketId}/reopen`); + if (res.success) setTicket(res.data); + } catch (err: any) { alert(err.message || '재오픈 실패'); } + }; + const handlePostComment = async () => { if (!newComment.trim()) return; try { @@ -124,9 +145,26 @@ export default function IssueDetailPage({ params }: { params: Promise<{ id: stri
{statusLabel[status]} - {!isClosed && ( - + )} + + + )} + {status === 'APPROVED' && isMgmt && ( + + )} + {status === 'REJECTED' && ( + )}