Skip to content

Commit 9b2f86e

Browse files
authored
Merge pull request #42 from INU-Software-Design/NEEIS-91-feature/report
[Neeis 91-feature/report] ์ƒ๋‹ด๋‚ด์—ญ ๋ณด๊ณ ์„œ PDF ์ƒ์„ฑ
2 parents b7d957c + 4f1eeb6 commit 9b2f86e

21 files changed

Lines changed: 1203 additions & 21 deletions

โ€Ž.DS_Storeโ€Ž

6 KB
Binary file not shown.

โ€Ž.gitignoreโ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ bin/
1717
!**/src/main/**/bin/
1818
!**/src/test/**/bin/
1919

20-
src/main/resources/**
20+
src/main/resources/firebase/**
21+
src/main/resources/application.properties
2122
application.properties
2223
.env
2324

โ€Žbuild.gradleโ€Ž

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jacoco {
3232
dependencies {
3333
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
3434
implementation 'org.springframework.boot:spring-boot-starter-web'
35+
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
3536
compileOnly 'org.projectlombok:lombok'
3637
runtimeOnly 'com.mysql:mysql-connector-j'
3738
annotationProcessor 'org.projectlombok:lombok'
@@ -60,12 +61,22 @@ dependencies {
6061

6162
// FCM
6263
implementation 'com.google.firebase:firebase-admin:9.4.3'
64+
65+
// Excel
66+
implementation 'org.apache.poi:poi:5.2.5'
67+
implementation 'org.apache.poi:poi-ooxml:5.2.5'
68+
69+
// PDF
70+
implementation 'com.openhtmltopdf:openhtmltopdf-slf4j:1.0.10' // ๋กœ๊ทธ์šฉ (์„ ํƒ)
71+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
72+
implementation 'com.itextpdf:itext7-core:7.1.2'
73+
implementation 'com.itextpdf:html2pdf:3.0.2'
6374
}
6475

76+
6577
tasks.named('test') {
6678
useJUnitPlatform()
6779
finalizedBy jacocoTestReport
68-
6980
}
7081

7182
// jacoco Report ์ƒ์„ฑ

โ€Žsrc/.DS_Storeโ€Ž

6 KB
Binary file not shown.

โ€Žsrc/main/.DS_Storeโ€Ž

6 KB
Binary file not shown.

โ€Žsrc/main/java/com/neeis/neeis/domain/counsel/CounselCategory.javaโ€Ž

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
package com.neeis.neeis.domain.counsel;
22

3-
public enum CounselCategory {
4-
// ๋Œ€ํ•™
5-
UNIVERSITY,
6-
7-
// ์ทจ์—…
8-
CAREER,
9-
10-
// ๊ฐ€์ •
11-
FAMILY,
3+
import lombok.Getter;
124

13-
// ํ•™์—…
14-
ACADEMIC,
5+
@Getter
6+
public enum CounselCategory {
7+
UNIVERSITY("๋Œ€ํ•™"),
8+
CAREER("์ทจ์—…"),
9+
FAMILY("๊ฐ€์ •"),
10+
ACADEMIC("ํ•™์—…"),
11+
PERSONAL("๊ฐœ์ธ"),
12+
OTHER("๊ธฐํƒ€");
1513

16-
// ๊ฐœ์ธ
17-
PERSONAL,
14+
private final String displayName;
1815

19-
// ๊ธฐํƒ€
20-
OTHER;
16+
CounselCategory(String displayName) {
17+
this.displayName = displayName;
18+
}
2119

2220
public static boolean exists(String value) {
2321
for(CounselCategory c : CounselCategory.values()) {

โ€Žsrc/main/java/com/neeis/neeis/domain/counsel/controller/CounselController.javaโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class CounselController {
2626
private final CounselService counselService;
2727

2828
@PostMapping
29-
@Operation(summary = "์ƒ๋‹ด ์ž‘์„ฑ", description = "๊ต์‚ฌ๊ฐ€ ํ•™์ƒ๊ณผ์˜ ์ƒ๋‹ด ๊ธฐ๋ก์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. <br>" +
29+
@Operation(summary = "[๊ต์‚ฌ ์ „์šฉ] ์ƒ๋‹ด ์ž‘์„ฑ", description = "๊ต์‚ฌ๊ฐ€ ํ•™์ƒ๊ณผ์˜ ์ƒ๋‹ด ๊ธฐ๋ก์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. <br>" +
3030
"์ƒ๋‹ด ์ข…๋ฅ˜: UNIVERSITY(๋Œ€ํ•™), CAREER(์ทจ์—…), FAMILY(๊ฐ€์ •), ACADEMIC(ํ•™์—…), PERSONAL(๊ฐœ์ธ), OTHER(๊ธฐํƒ€) ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค." )
3131
public ResponseEntity<CommonResponse<CounselResponseDto>> postCounsel(
3232
@AuthenticationPrincipal UserDetails userDetails,
@@ -55,7 +55,7 @@ public ResponseEntity<CommonResponse<List<CounselDetailDto>>> getCounsels(
5555
}
5656

5757
@PutMapping("/{counselId}")
58-
@Operation(summary = "์ƒ๋‹ด ์ˆ˜์ •", description = "์ƒ๋‹ด ๋‚ด์šฉ์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.<br> " +
58+
@Operation(summary = "[๊ต์‚ฌ ์ „์šฉ] ์ƒ๋‹ด ์ˆ˜์ •", description = "์ƒ๋‹ด ๋‚ด์šฉ์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.<br> " +
5959
"์กฐํšŒ์‹œ ์ œ๊ณต๋ฐ›์€ counselId๋ฅผ path parameter๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
6060
public ResponseEntity<CommonResponse<CounselDetailDto>> updateCounsel(
6161
@AuthenticationPrincipal UserDetails userDetails,
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.neeis.neeis.domain.counsel.controller;
2+
3+
import com.neeis.neeis.domain.counsel.service.CounselPdfService;
4+
import com.neeis.neeis.domain.counsel.service.CounselService;
5+
import com.neeis.neeis.global.jwt.CustomUserDetails;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.Parameter;
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.springframework.http.HttpHeaders;
14+
import org.springframework.http.MediaType;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
17+
import org.springframework.web.bind.annotation.*;
18+
19+
import java.time.LocalDateTime;
20+
import java.time.format.DateTimeFormatter;
21+
22+
/**
23+
* ์ƒ๋‹ด ๋ณด๊ณ ์„œ PDF API ์ปจํŠธ๋กค๋Ÿฌ
24+
*
25+
* ๊ธฐ์กด CounselService๋ฅผ ํ†ตํ•ด ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ƒ๋‹ด ๋ฐ์ดํ„ฐ๋ฅผ PDF๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
26+
*/
27+
@Slf4j
28+
@RestController
29+
@RequestMapping("/reports/counsels")
30+
@RequiredArgsConstructor
31+
@Tag(name = "์ƒ๋‹ด PDF", description = "์ƒ๋‹ด ๋ณด๊ณ ์„œ PDF ์ƒ์„ฑ API")
32+
public class CounselPdfController {
33+
34+
// โœ… CounselService ์‚ฌ์šฉ (CounselPdfService ๋Œ€์‹ )
35+
private final CounselPdfService counselPdfService;
36+
37+
@GetMapping("/{counselId}/pdf")
38+
@Operation(summary = "๊ฐœ๋ณ„ ์ƒ๋‹ด PDF ์ƒ์„ฑ", description = "ํŠน์ • ์ƒ๋‹ด์— ๋Œ€ํ•œ PDF ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.")
39+
@ApiResponses({
40+
@ApiResponse(responseCode = "200", description = "PDF ์ƒ์„ฑ ์„ฑ๊ณต"),
41+
@ApiResponse(responseCode = "403", description = "์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"),
42+
@ApiResponse(responseCode = "404", description = "์ƒ๋‹ด์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ")
43+
})
44+
public ResponseEntity<byte[]> generateSingleCounselPdf(
45+
@Parameter(description = "์ƒ๋‹ด ID", required = true)
46+
@PathVariable Long counselId,
47+
@AuthenticationPrincipal CustomUserDetails userDetails) {
48+
49+
log.info("๊ฐœ๋ณ„ ์ƒ๋‹ด PDF ์ƒ์„ฑ ์š”์ฒญ - ์‚ฌ์šฉ์ž: {}, ์ƒ๋‹ดID: {}", userDetails.getUsername(), counselId);
50+
51+
try {
52+
// CounselService๋ฅผ ํ†ตํ•ด PDF ์ƒ์„ฑ (์‹ค์ œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ)
53+
byte[] pdfBytes = counselPdfService.generateSingleCounselPdf(userDetails.getUsername(), counselId);
54+
55+
// ํŒŒ์ผ๋ช… ์ƒ์„ฑ (์ƒ๋‹ดID_๋‚ ์งœ.pdf)
56+
String filename = String.format("counsel_report_%d_%s.pdf",
57+
counselId,
58+
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")));
59+
60+
// HTTP ์‘๋‹ต ํ—ค๋” ์„ค์ •
61+
HttpHeaders headers = createPdfHeaders(filename, pdfBytes.length);
62+
63+
log.info("๊ฐœ๋ณ„ ์ƒ๋‹ด PDF ์ƒ์„ฑ ์™„๋ฃŒ - ํŒŒ์ผ๋ช…: {}, ํฌ๊ธฐ: {} bytes", filename, pdfBytes.length);
64+
65+
return ResponseEntity.ok()
66+
.headers(headers)
67+
.body(pdfBytes);
68+
69+
} catch (Exception e) {
70+
log.error("๊ฐœ๋ณ„ ์ƒ๋‹ด PDF ์ƒ์„ฑ ์‹คํŒจ - ์ƒ๋‹ดID: {}, ์˜ค๋ฅ˜: {}", counselId, e.getMessage(), e);
71+
throw e;
72+
}
73+
}
74+
75+
@GetMapping("/students/{studentId}/history/pdf")
76+
@Operation(summary = "ํ•™์ƒ๋ณ„ ์ƒ๋‹ด ์ด๋ ฅ PDF ์ƒ์„ฑ", description = "ํŠน์ • ํ•™์ƒ์˜ ๋ชจ๋“  ์ƒ๋‹ด ์ด๋ ฅ์„ PDF๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.")
77+
@ApiResponses({
78+
@ApiResponse(responseCode = "200", description = "PDF ์ƒ์„ฑ ์„ฑ๊ณต"),
79+
@ApiResponse(responseCode = "403", description = "์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"),
80+
@ApiResponse(responseCode = "404", description = "์ƒ๋‹ด ์ด๋ ฅ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ")
81+
})
82+
public ResponseEntity<byte[]> generateStudentCounselHistoryPdf(
83+
@Parameter(description = "ํ•™์ƒ ID", required = true)
84+
@PathVariable Long studentId,
85+
@AuthenticationPrincipal CustomUserDetails userDetails) {
86+
87+
log.info("ํ•™์ƒ ์ƒ๋‹ด ์ด๋ ฅ PDF ์ƒ์„ฑ ์š”์ฒญ - ์‚ฌ์šฉ์ž: {}, ํ•™์ƒID: {}", userDetails.getUsername(), studentId);
88+
89+
try {
90+
// CounselService๋ฅผ ํ†ตํ•ด PDF ์ƒ์„ฑ (์‹ค์ œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ)
91+
byte[] pdfBytes = counselPdfService.generateStudentCounselHistoryPdf(userDetails.getUsername(), studentId);
92+
93+
// ํŒŒ์ผ๋ช… ์ƒ์„ฑ (student_counsel_history_ํ•™์ƒID_๋‚ ์งœ.pdf)
94+
String filename = String.format("student_counsel_history_%d_%s.pdf",
95+
studentId,
96+
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")));
97+
98+
// HTTP ์‘๋‹ต ํ—ค๋” ์„ค์ •
99+
HttpHeaders headers = createPdfHeaders(filename, pdfBytes.length);
100+
101+
log.info("ํ•™์ƒ ์ƒ๋‹ด ์ด๋ ฅ PDF ์ƒ์„ฑ ์™„๋ฃŒ - ํŒŒ์ผ๋ช…: {}, ํฌ๊ธฐ: {} bytes", filename, pdfBytes.length);
102+
103+
return ResponseEntity.ok()
104+
.headers(headers)
105+
.body(pdfBytes);
106+
107+
} catch (Exception e) {
108+
log.error("ํ•™์ƒ ์ƒ๋‹ด ์ด๋ ฅ PDF ์ƒ์„ฑ ์‹คํŒจ - ํ•™์ƒID: {}, ์˜ค๋ฅ˜: {}", studentId, e.getMessage(), e);
109+
throw e;
110+
}
111+
}
112+
113+
/**
114+
* PDF ์‘๋‹ต์„ ์œ„ํ•œ HTTP ํ—ค๋” ์ƒ์„ฑ
115+
*/
116+
private HttpHeaders createPdfHeaders(String filename, int contentLength) {
117+
HttpHeaders headers = new HttpHeaders();
118+
headers.setContentType(MediaType.APPLICATION_PDF);
119+
headers.setContentDispositionFormData("attachment", filename);
120+
headers.setContentLength(contentLength);
121+
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
122+
headers.add("Pragma", "no-cache");
123+
headers.add("Expires", "0");
124+
return headers;
125+
}
126+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.neeis.neeis.domain.counsel.dto.pdf;
2+
3+
import com.neeis.neeis.domain.counsel.Counsel;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.time.LocalDateTime;
8+
import java.time.format.DateTimeFormatter;
9+
10+
/**
11+
* ์ƒ๋‹ด PDF ์ƒ์„ฑ์šฉ DTO
12+
*/
13+
@Getter
14+
@Builder
15+
public class CounselPdfDto {
16+
private final String studentName;
17+
private final String teacherName;
18+
private final String category;
19+
private final String counselDate;
20+
private final String counselTime;
21+
private final String content;
22+
private final String nextPlan;
23+
private final String generatedDate;
24+
25+
26+
/**
27+
* Counsel ์—”ํ‹ฐํ‹ฐ๋ฅผ PDF DTO๋กœ ๋ณ€ํ™˜
28+
*/
29+
public static CounselPdfDto fromEntity(Counsel counsel) {
30+
31+
return CounselPdfDto.builder()
32+
.studentName(counsel.getStudent().getName())
33+
.teacherName(counsel.getTeacher().getName())
34+
.category(getCategoryKoreanName(counsel.getCategory().name()))
35+
.counselDate(counsel.getDateTime().format(DateTimeFormatter.ofPattern("yyyy๋…„ MM์›” dd์ผ")))
36+
.content(counsel.getContent())
37+
.nextPlan(counsel.getNextPlan())
38+
.generatedDate(counsel.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy๋…„ MM์›” dd์ผ HH์‹œ mm๋ถ„")))
39+
.build();
40+
}
41+
42+
/**
43+
* ์ƒ๋‹ด ์นดํ…Œ๊ณ ๋ฆฌ ์˜๋ฌธ์„ ํ•œ๊ธ€๋กœ ๋ณ€ํ™˜
44+
*/
45+
private static String getCategoryKoreanName(String categoryName) {
46+
return switch (categoryName) {
47+
case "UNIVERSITY" -> "๋Œ€ํ•™์ƒ๋‹ด";
48+
case "CAREER" -> "์ง„๋กœ์ƒ๋‹ด";
49+
case "ACADEMIC" -> "ํ•™์—…์ƒ๋‹ด";
50+
case "PERSONAL" -> "๊ฐœ์ธ์ƒ๋‹ด";
51+
case "FAMILY" -> "๊ฐ€์ •์ƒ๋‹ด";
52+
case "OTHER" -> "๊ธฐํƒ€";
53+
default -> categoryName;
54+
};
55+
}
56+
}

0 commit comments

Comments
ย (0)