Skip to content

Commit 62ec35d

Browse files
authored
[feat] 고객센터 문의 기능 구현 (#297)
* [refactor] 고객센터 패키지명 통합 리팩토링 * [feat] 고객센터 문의 기능 구현
1 parent 7dd565f commit 62ec35d

20 files changed

+2365
-0
lines changed
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
package com.back.domain.support.inquiry.controller;
2+
3+
import com.back.domain.support.inquiry.dto.request.InquiryCreateRequest;
4+
import com.back.domain.support.inquiry.dto.request.InquiryReplyRequest;
5+
import com.back.domain.support.inquiry.dto.request.InquiryUpdateRequest;
6+
import com.back.domain.support.inquiry.dto.response.InquiryDetailResponse;
7+
import com.back.domain.support.inquiry.dto.response.InquiryListResponse;
8+
import com.back.domain.support.inquiry.service.InquiryService;
9+
import com.back.domain.user.entity.User;
10+
import com.back.domain.user.repository.UserRepository;
11+
import com.back.global.exception.ServiceException;
12+
import com.back.global.rsData.RsData;
13+
import com.back.global.security.auth.CustomUserDetails;
14+
import io.swagger.v3.oas.annotations.Operation;
15+
import io.swagger.v3.oas.annotations.Parameter;
16+
import io.swagger.v3.oas.annotations.tags.Tag;
17+
import jakarta.validation.Valid;
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.extern.slf4j.Slf4j;
20+
import org.springframework.data.domain.PageRequest;
21+
import org.springframework.data.domain.Pageable;
22+
import org.springframework.http.ResponseEntity;
23+
import org.springframework.security.access.prepost.PreAuthorize;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
26+
import org.springframework.web.bind.annotation.*;
27+
28+
@Slf4j
29+
@RestController
30+
@RequestMapping("/api/support/inquiries")
31+
@RequiredArgsConstructor
32+
@Tag(name = "문의", description = "1:1 문의 관련 API")
33+
public class InquiryController {
34+
35+
private final InquiryService inquiryService;
36+
private final UserRepository userRepository;
37+
38+
/**
39+
* 공개 문의 목록 조회 (비로그인)
40+
*/
41+
@GetMapping("/public")
42+
@Operation(
43+
summary = "공개 문의 목록 조회 (비로그인)",
44+
description = "비밀문의를 제외한 공개 문의 목록을 조회합니다."
45+
)
46+
public ResponseEntity<RsData<InquiryListResponse>> getPublicInquiries(
47+
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1")
48+
@RequestParam(defaultValue = "1") int page,
49+
50+
@Parameter(description = "페이지 크기", example = "10")
51+
@RequestParam(defaultValue = "10") int size) {
52+
53+
log.info("공개 문의 목록 조회 (비로그인) - page: {}, size: {}", page, size);
54+
55+
Pageable pageable = PageRequest.of(page - 1, size);
56+
InquiryListResponse response = inquiryService.getPublicInquiries(pageable);
57+
58+
return ResponseEntity.ok(
59+
RsData.of("200", "공개 문의 목록 조회 성공", response)
60+
);
61+
}
62+
63+
/**
64+
* 공개 문의 + 내 문의 목록 조회 (로그인)
65+
*/
66+
@GetMapping
67+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
68+
@Operation(
69+
summary = "문의 목록 조회 (로그인)",
70+
description = "공개 문의 + 본인의 비밀문의를 조회합니다."
71+
)
72+
public ResponseEntity<RsData<InquiryListResponse>> getInquiries(
73+
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1")
74+
@RequestParam(defaultValue = "1") int page,
75+
76+
@Parameter(description = "페이지 크기", example = "10")
77+
@RequestParam(defaultValue = "10") int size,
78+
79+
@Parameter(hidden = true)
80+
@AuthenticationPrincipal CustomUserDetails userDetails) {
81+
82+
log.info("문의 목록 조회 (로그인) - userId: {}, page: {}", userDetails.getUserId(), page);
83+
84+
User user = getUserFromDetails(userDetails);
85+
Pageable pageable = PageRequest.of(page - 1, size);
86+
InquiryListResponse response = inquiryService.getPublicInquiriesOrMine(user, pageable);
87+
88+
return ResponseEntity.ok(
89+
RsData.of("200", "문의 목록 조회 성공", response)
90+
);
91+
}
92+
93+
/**
94+
* 내 문의만 조회
95+
*/
96+
@GetMapping("/my")
97+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
98+
@Operation(
99+
summary = "내 문의 목록 조회",
100+
description = "본인이 작성한 문의만 조회합니다."
101+
)
102+
public ResponseEntity<RsData<InquiryListResponse>> getMyInquiries(
103+
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1")
104+
@RequestParam(defaultValue = "1") int page,
105+
106+
@Parameter(description = "페이지 크기", example = "10")
107+
@RequestParam(defaultValue = "10") int size,
108+
109+
@Parameter(hidden = true)
110+
@AuthenticationPrincipal CustomUserDetails userDetails) {
111+
112+
log.info("내 문의 목록 조회 - userId: {}, page: {}", userDetails.getUserId(), page);
113+
114+
User user = getUserFromDetails(userDetails);
115+
Pageable pageable = PageRequest.of(page - 1, size);
116+
InquiryListResponse response = inquiryService.getMyInquiries(user, pageable);
117+
118+
return ResponseEntity.ok(
119+
RsData.of("200", "내 문의 목록 조회 성공", response)
120+
);
121+
}
122+
123+
/**
124+
* 전체 문의 목록 조회 (관리자 전용)
125+
*/
126+
@GetMapping("/admin/all")
127+
@PreAuthorize("hasRole('ADMIN')")
128+
@Operation(
129+
summary = "전체 문의 목록 조회 (관리자 전용)",
130+
description = "비밀문의를 포함한 모든 문의를 조회합니다."
131+
)
132+
public ResponseEntity<RsData<InquiryListResponse>> getAllInquiriesForAdmin(
133+
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1")
134+
@RequestParam(defaultValue = "1") int page,
135+
136+
@Parameter(description = "페이지 크기", example = "10")
137+
@RequestParam(defaultValue = "10") int size,
138+
139+
@Parameter(hidden = true)
140+
@AuthenticationPrincipal CustomUserDetails adminDetails) {
141+
142+
log.info("전체 문의 목록 조회 (관리자) - adminId: {}, page: {}", adminDetails.getUserId(), page);
143+
144+
Pageable pageable = PageRequest.of(page - 1, size);
145+
InquiryListResponse response = inquiryService.getAllInquiriesForAdmin(pageable);
146+
147+
return ResponseEntity.ok(
148+
RsData.of("200", "전체 문의 목록 조회 성공", response)
149+
);
150+
}
151+
152+
// ========================================
153+
// 문의 CRUD
154+
// ========================================
155+
/**
156+
* 문의 상세 조회
157+
*/
158+
@GetMapping("/{inquiryId}")
159+
@Operation(
160+
summary = "문의 상세 조회",
161+
description = "문의 상세 정보를 조회합니다. 비밀문의는 작성자와 관리자만 조회 가능합니다."
162+
)
163+
public ResponseEntity<RsData<InquiryDetailResponse>> getInquiry(
164+
@PathVariable Long inquiryId,
165+
@Parameter(hidden = true)
166+
Authentication authentication) {
167+
168+
log.info("문의 상세 조회 - inquiryId: {}", inquiryId);
169+
170+
Long currentUserId = null;
171+
boolean isAdmin = false;
172+
173+
if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails userDetails) {
174+
currentUserId = userDetails.getUserId();
175+
isAdmin = userDetails.getAuthorities().stream()
176+
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
177+
}
178+
179+
InquiryDetailResponse response = inquiryService.getInquiry(inquiryId, currentUserId, isAdmin);
180+
181+
return ResponseEntity.ok(
182+
RsData.of("200", "문의 상세 조회 성공", response)
183+
);
184+
}
185+
186+
/**
187+
* 문의 작성
188+
*/
189+
@PostMapping
190+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
191+
@Operation(
192+
summary = "문의 작성",
193+
description = "새로운 문의를 작성합니다. 첨부파일은 최대 3개까지 업로드 가능합니다."
194+
)
195+
public ResponseEntity<RsData<Long>> createInquiry(
196+
@Valid @ModelAttribute InquiryCreateRequest request,
197+
198+
@Parameter(hidden = true)
199+
@AuthenticationPrincipal CustomUserDetails userDetails) {
200+
201+
log.info("문의 작성 요청 - userId: {}, category: {}", userDetails.getUserId(), request.category());
202+
203+
User user = getUserFromDetails(userDetails);
204+
Long inquiryId = inquiryService.createInquiry(request, user);
205+
206+
return ResponseEntity.ok(
207+
RsData.of("200", "문의가 성공적으로 등록되었습니다.", inquiryId)
208+
);
209+
}
210+
211+
/**
212+
* 문의 수정
213+
*/
214+
@PutMapping("/{inquiryId}")
215+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
216+
@Operation(
217+
summary = "문의 수정",
218+
description = "문의를 수정합니다. 본인이 작성한 문의만 수정 가능합니다."
219+
)
220+
public ResponseEntity<RsData<Void>> updateInquiry(
221+
@Parameter(description = "문의 ID", example = "1", required = true)
222+
@PathVariable Long inquiryId,
223+
224+
@Valid @ModelAttribute InquiryUpdateRequest request,
225+
226+
@Parameter(hidden = true)
227+
@AuthenticationPrincipal CustomUserDetails userDetails) {
228+
229+
log.info("문의 수정 요청 - userId: {}, inquiryId: {}", userDetails.getUserId(), inquiryId);
230+
231+
inquiryService.updateInquiry(inquiryId, request, userDetails.getUserId());
232+
233+
return ResponseEntity.ok(
234+
RsData.of("200", "문의가 성공적으로 수정되었습니다.")
235+
);
236+
}
237+
238+
/**
239+
* 문의 삭제
240+
*/
241+
@DeleteMapping("/{inquiryId}")
242+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
243+
@Operation(
244+
summary = "문의 삭제",
245+
description = "문의를 삭제합니다. 본인이 작성한 문의 또는 관리자만 삭제 가능합니다."
246+
)
247+
public ResponseEntity<RsData<Void>> deleteInquiry(
248+
@Parameter(description = "문의 ID", example = "1", required = true)
249+
@PathVariable Long inquiryId,
250+
251+
@Parameter(hidden = true)
252+
@AuthenticationPrincipal CustomUserDetails userDetails) {
253+
254+
log.info("문의 삭제 요청 - userId: {}, inquiryId: {}", userDetails.getUserId(), inquiryId);
255+
256+
boolean isAdmin = userDetails.getAuthorities().stream()
257+
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
258+
259+
inquiryService.deleteInquiry(inquiryId, userDetails.getUserId(), isAdmin);
260+
261+
return ResponseEntity.ok(
262+
RsData.of("200", "문의가 삭제되었습니다.")
263+
);
264+
}
265+
266+
// ========================================
267+
// 댓글 CRUD
268+
// ========================================
269+
270+
/**
271+
* 댓글 작성
272+
*/
273+
@PostMapping("/{inquiryId}/replies")
274+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
275+
@Operation(
276+
summary = "댓글 작성",
277+
description = "문의에 댓글을 작성합니다. 관리자 답변 시 문의 상태가 '답변완료'로 변경됩니다."
278+
)
279+
public ResponseEntity<RsData<Long>> createReply(
280+
@Parameter(description = "문의 ID", example = "1", required = true)
281+
@PathVariable Long inquiryId,
282+
283+
@Valid @RequestBody InquiryReplyRequest request,
284+
285+
@Parameter(hidden = true)
286+
@AuthenticationPrincipal CustomUserDetails userDetails) {
287+
288+
log.info("댓글 작성 요청 - userId: {}, inquiryId: {}", userDetails.getUserId(), inquiryId);
289+
290+
User user = getUserFromDetails(userDetails);
291+
boolean isAdmin = userDetails.getAuthorities().stream()
292+
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
293+
294+
Long replyId = inquiryService.createReply(inquiryId, request, user, isAdmin);
295+
296+
return ResponseEntity.ok(
297+
RsData.of("200", "댓글이 성공적으로 등록되었습니다.", replyId)
298+
);
299+
}
300+
301+
/**
302+
* 댓글 수정
303+
*/
304+
@PutMapping("/{inquiryId}/replies/{replyId}")
305+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
306+
@Operation(
307+
summary = "댓글 수정",
308+
description = "댓글을 수정합니다. 본인이 작성한 댓글만 수정 가능합니다."
309+
)
310+
public ResponseEntity<RsData<Void>> updateReply(
311+
@Parameter(description = "문의 ID", example = "1", required = true)
312+
@PathVariable Long inquiryId,
313+
314+
@Parameter(description = "댓글 ID", example = "1", required = true)
315+
@PathVariable Long replyId,
316+
317+
@Valid @RequestBody InquiryReplyRequest request,
318+
319+
@Parameter(hidden = true)
320+
@AuthenticationPrincipal CustomUserDetails userDetails) {
321+
322+
log.info("댓글 수정 요청 - userId: {}, replyId: {}", userDetails.getUserId(), replyId);
323+
324+
inquiryService.updateReply(replyId, request, userDetails.getUserId());
325+
326+
return ResponseEntity.ok(
327+
RsData.of("200", "댓글이 성공적으로 수정되었습니다.")
328+
);
329+
}
330+
331+
/**
332+
* 댓글 삭제
333+
*/
334+
@DeleteMapping("/{inquiryId}/replies/{replyId}")
335+
@PreAuthorize("hasAnyRole('USER', 'ARTIST', 'ADMIN')")
336+
@Operation(
337+
summary = "댓글 삭제",
338+
description = "댓글을 삭제합니다. 본인이 작성한 댓글 또는 관리자만 삭제 가능합니다."
339+
)
340+
public ResponseEntity<RsData<Void>> deleteReply(
341+
@Parameter(description = "문의 ID", example = "1", required = true)
342+
@PathVariable Long inquiryId,
343+
344+
@Parameter(description = "댓글 ID", example = "1", required = true)
345+
@PathVariable Long replyId,
346+
347+
@Parameter(hidden = true)
348+
@AuthenticationPrincipal CustomUserDetails userDetails) {
349+
350+
log.info("댓글 삭제 요청 - userId: {}, replyId: {}", userDetails.getUserId(), replyId);
351+
352+
boolean isAdmin = userDetails.getAuthorities().stream()
353+
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
354+
355+
inquiryService.deleteReply(replyId, userDetails.getUserId(), isAdmin);
356+
357+
return ResponseEntity.ok(
358+
RsData.of("200", "댓글이 삭제되었습니다.")
359+
);
360+
}
361+
362+
// ===== 헬퍼 메서드 =====
363+
364+
/**
365+
* CustomUserDetails에서 User 엔티티 조회
366+
*/
367+
private User getUserFromDetails(CustomUserDetails userDetails) {
368+
return userRepository.findById(userDetails.getUserId())
369+
.orElseThrow(() -> new ServiceException("404", "사용자를 찾을 수 없습니다."));
370+
}
371+
372+
}

0 commit comments

Comments
 (0)