Skip to content

Commit 3b10cc8

Browse files
authored
[fix] 댓글 삭제 관련 오류 해결#303 (#304)
* fix : dataSet * fix: getCocktailComments * fix : delete error * fix : batch
1 parent 9f86860 commit 3b10cc8

File tree

13 files changed

+770
-152
lines changed

13 files changed

+770
-152
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,7 @@ terraform/secrets.tf
5555
### Claude AI ###
5656
CLAUDE.md
5757
.claude/
58+
59+
## dataSet ##
60+
cocktails_ver00.csv
61+
cocktails_ver01.csv

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies {
4040
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
4141
implementation("org.springframework.boot:spring-boot-starter-batch")
4242
testImplementation("org.springframework.batch:spring-batch-test")
43+
4344
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
4445
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
4546
compileOnly("org.projectlombok:lombok")

src/main/java/com/back/domain/cocktail/comment/repository/CocktailCommentRepository.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
@Repository
1111
public interface CocktailCommentRepository extends JpaRepository<CocktailComment, Long> {
1212

13-
List<CocktailComment> findTop10ByCocktailIdOrderByIdDesc(Long cocktailId);
13+
List<CocktailComment> findTop10ByCocktailIdAndStatusInOrderByIdDesc(
14+
Long cocktailId, List<CommentStatus> statuses
15+
);
1416

15-
List<CocktailComment> findTop10ByCocktailIdAndIdLessThanOrderByIdDesc(Long cocktailId, Long lastId);
17+
List<CocktailComment> findTop10ByCocktailIdAndStatusInAndIdLessThanOrderByIdDesc(
18+
Long cocktailId, List<CommentStatus> statuses, Long lastId
19+
);
1620

1721
boolean existsByCocktailIdAndUserIdAndStatusNot(Long cocktailId, Long id, CommentStatus status);
1822
}

src/main/java/com/back/domain/cocktail/comment/service/CocktailCommentService.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.back.domain.cocktail.repository.CocktailRepository;
1010
import com.back.domain.post.comment.enums.CommentStatus;
1111
import com.back.domain.user.entity.User;
12+
import com.back.global.exception.UnauthorizedException;
1213
import com.back.global.rq.Rq;
1314
import lombok.RequiredArgsConstructor;
1415
import org.springframework.stereotype.Service;
@@ -46,6 +47,7 @@ public CocktailCommentResponseDto createCocktailComment(Long cocktailId, Cocktai
4647
.cocktail(cocktail)
4748
.user(user)
4849
.content(reqBody.content())
50+
.status(reqBody.status())
4951
.build();
5052

5153
return new CocktailCommentResponseDto(cocktailCommentRepository.save(cocktailComment));
@@ -54,17 +56,34 @@ public CocktailCommentResponseDto createCocktailComment(Long cocktailId, Cocktai
5456
// 칵테일 댓글 다건 조회 로직 (무한스크롤)
5557
@Transactional(readOnly = true)
5658
public List<CocktailCommentResponseDto> getCocktailComments(Long cocktailId, Long lastId) {
59+
User actor = rq.getActor(); // 서비스에서 호출 가능
60+
61+
if (actor == null) {
62+
throw new UnauthorizedException("로그인이 필요합니다.");
63+
}
64+
Long currentUserId = actor.getId();
65+
List<CocktailComment> comments;
66+
5767
if (lastId == null) {
58-
return cocktailCommentRepository.findTop10ByCocktailIdOrderByIdDesc(cocktailId)
59-
.stream()
60-
.map(CocktailCommentResponseDto::new)
61-
.toList();
68+
comments = cocktailCommentRepository
69+
.findTop10ByCocktailIdAndStatusInOrderByIdDesc(cocktailId, List.of(CommentStatus.PUBLIC, CommentStatus.PRIVATE)
70+
);
6271
} else {
63-
return cocktailCommentRepository.findTop10ByCocktailIdAndIdLessThanOrderByIdDesc(cocktailId, lastId)
64-
.stream()
65-
.map(CocktailCommentResponseDto::new)
66-
.toList();
72+
comments = cocktailCommentRepository
73+
.findTop10ByCocktailIdAndStatusInAndIdLessThanOrderByIdDesc(cocktailId, List.of(CommentStatus.PUBLIC, CommentStatus.PRIVATE),
74+
lastId);
6775
}
76+
77+
return comments.stream()
78+
.filter(comment ->{
79+
if(comment.getStatus() == CommentStatus.PUBLIC) return true;
80+
if(comment.getStatus() == CommentStatus.PRIVATE) {
81+
return comment.getUser().getId().equals(currentUserId);
82+
}
83+
return false;
84+
})
85+
.map(CocktailCommentResponseDto::new)
86+
.toList();
6887
}
6988

7089
// 칵테일 댓글 단건 조회 로직

src/main/java/com/back/domain/cocktail/service/CocktailService.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,19 @@ private String convertFractions(String ingredient) {
133133
if (ingredient == null) return null;
134134

135135
// 치환 테이블 생성
136-
Map<String, String> fractionMap = Map.of(
137-
"1/2", "½",
138-
"1/3", "⅓",
139-
"2/3", "⅔",
140-
"1/4", "¼",
141-
"3/4", "¾",
142-
"1/8", "⅛",
143-
"3/8", "⅜",
144-
"5/8", "⅝",
145-
"7/8", "⅞"
136+
Map<String, String> fractionMap = Map.ofEntries(
137+
Map.entry("1/2", "½"),
138+
Map.entry("1/3", "⅓"),
139+
Map.entry("2/3", "⅔"),
140+
Map.entry("1/4", "¼"),
141+
Map.entry("3/4", "¾"),
142+
Map.entry("1/8", "⅛"),
143+
Map.entry("3/8", "⅜"),
144+
Map.entry("5/8", "⅝"),
145+
Map.entry("7/8", "⅞"),
146+
Map.entry("1/5", "⅕"),
147+
Map.entry("2/5", "⅖"),
148+
Map.entry("1/6", "⅙")
146149
);
147150

148151
// 테이블 기반 치환
@@ -176,7 +179,7 @@ private List<IngredientDto> parseIngredients(String ingredientStr) {
176179

177180
// (숫자 + 선택적 분수) + (공백) + (단위)
178181
Pattern pattern = Pattern.compile(
179-
"^([0-9]*\\s*[½⅓⅔¼¾⅛⅜⅝⅞]?)\\s*(.*)$",
182+
"^([0-9]*\\s*[½⅓⅔¼¾⅛⅜⅝⅞⅕⅖⅙]?)\\s*(.*)$",
180183
Pattern.UNICODE_CHARACTER_CLASS
181184
);
182185
Matcher matcher = pattern.matcher(amountUnit);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.back.global.appConfig;
2+
3+
import com.back.global.standard.util.DecimalToFractionConverter;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.batch.core.Job;
6+
import org.springframework.batch.core.Step;
7+
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
8+
import org.springframework.batch.core.job.builder.JobBuilder;
9+
import org.springframework.batch.core.repository.JobRepository;
10+
import org.springframework.batch.core.step.builder.StepBuilder;
11+
import org.springframework.batch.item.ItemProcessor;
12+
import org.springframework.batch.item.file.FlatFileItemReader;
13+
import org.springframework.batch.item.file.FlatFileItemWriter;
14+
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
15+
import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;
16+
import org.springframework.batch.item.file.mapping.PassThroughLineMapper;
17+
import org.springframework.batch.item.file.transform.PassThroughLineAggregator;
18+
import org.springframework.context.annotation.Bean;
19+
import org.springframework.context.annotation.Configuration;
20+
import org.springframework.core.io.FileSystemResource;
21+
import org.springframework.transaction.PlatformTransactionManager;
22+
23+
// DecimalToFractionConverter가 있다고 가정합니다.
24+
// import com.example.DecimalToFractionConverter;
25+
26+
@Configuration
27+
@EnableBatchProcessing
28+
@RequiredArgsConstructor
29+
public class CocktailBatchConfig {
30+
31+
private final JobRepository jobRepository;
32+
private final PlatformTransactionManager transactionManager;
33+
34+
private static final String INPUT_PATH = "src/main/resources/cocktails.csv";
35+
private static final String OUTPUT_PATH = "src/main/resources/cocktails_clean.csv";
36+
37+
// 1.Reader: CSV 파일 한 줄씩 읽기
38+
@Bean
39+
public FlatFileItemReader<String> reader() {
40+
return new FlatFileItemReaderBuilder<String>()
41+
.name("cocktailReader")
42+
.resource(new FileSystemResource(INPUT_PATH))
43+
.lineMapper(new PassThroughLineMapper())
44+
.build();
45+
}
46+
47+
// 2️⃣ Processor: ingredient 컬럼(6번째)만 DecimalToFractionConverter로 변환
48+
@Bean
49+
public ItemProcessor<String, String> processor() {
50+
return line -> {
51+
// CSV 한 줄을 안전하게 split
52+
String[] columns = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);
53+
54+
// Header인지 확인: "ingredient"라는 컬럼 이름으로 간단 체크
55+
if (columns.length > 6 && !columns[6].equalsIgnoreCase("ingredient")) {
56+
// DecimalToFractionConverter는 별도로 구현되어 있어야 합니다.
57+
columns[6] = DecimalToFractionConverter.convert(columns[6]);
58+
}
59+
60+
// 다시 CSV 문자열로 합치기
61+
return String.join(",", columns);
62+
};
63+
}
64+
65+
// 3️⃣ Writer: 변환된 CSV 출력 (변화 없음)
66+
@Bean
67+
public FlatFileItemWriter<String> writer() {
68+
return new FlatFileItemWriterBuilder<String>()
69+
.name("cocktailWriter")
70+
.resource(new FileSystemResource(OUTPUT_PATH))
71+
.lineAggregator(new PassThroughLineAggregator<>())
72+
.build();
73+
}
74+
75+
// 4️⃣ Step: Chunk 단위 처리 (StepBuilderFactory 대체)
76+
@Bean
77+
public Step convertStep() {
78+
// StepBuilder를 직접 생성하고, jobRepository와 transactionManager를 주입합니다.
79+
return new StepBuilder("convertStep", jobRepository)
80+
// chunk 메서드에 transactionManager를 인수로 전달합니다.
81+
.<String, String>chunk(5, transactionManager)
82+
.reader(reader())
83+
.processor(processor())
84+
.writer(writer())
85+
.build();
86+
}
87+
88+
// 5️⃣ Job 정의 (JobBuilderFactory 대체)
89+
@Bean
90+
public Job convertJob() {
91+
// JobBuilder를 직접 생성하고, jobRepository를 주입합니다.
92+
return new JobBuilder("convertJob", jobRepository)
93+
.start(convertStep())
94+
.build();
95+
}
96+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.back.global.exception;
2+
3+
public class UnauthorizedException extends ServiceException {
4+
5+
private static final int UNAUTHORIZED_CODE = 401;
6+
private static final String UNAUTHORIZED_MSG = "로그인이 필요합니다.";
7+
8+
public UnauthorizedException() {
9+
super(UNAUTHORIZED_CODE, UNAUTHORIZED_MSG);
10+
}
11+
12+
public UnauthorizedException(String msg) {
13+
super(UNAUTHORIZED_CODE, msg);
14+
}
15+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.back.global.standard.util;
2+
3+
import java.io.*;
4+
5+
public class CocktailDataCleaner {
6+
public static void main(String[] args) {
7+
String inputPath = "src/main/resources/cocktails.csv"; // 원본 파일 경로
8+
String outputPath = "src/main/resources/cocktails_clean.csv"; // 정제 후 파일 경로
9+
10+
try (
11+
BufferedReader br = new BufferedReader(new FileReader(inputPath));
12+
BufferedWriter bw = new BufferedWriter(new FileWriter(outputPath))
13+
) {
14+
String line;
15+
boolean isHeader = true;
16+
17+
while ((line = br.readLine()) != null) {
18+
// CSV 안의 쉼표와 큰따옴표를 안전하게 처리
19+
String[] columns = splitCsvLine(line);
20+
21+
if (isHeader) {
22+
bw.write(String.join(",", columns));
23+
bw.newLine();
24+
isHeader = false;
25+
continue;
26+
}
27+
28+
// ✅ 6번째 인덱스(ingredient) 컬럼만 변환
29+
if (columns.length > 6) {
30+
columns[6] = DecimalToFractionConverter.convert(columns[6]);
31+
}
32+
33+
bw.write(String.join(",", columns));
34+
bw.newLine();
35+
}
36+
37+
System.out.println("✅ 칵테일 데이터 정제가 완료되었습니다: " + outputPath);
38+
39+
} catch (IOException e) {
40+
System.err.println("❌ 파일 처리 중 오류 발생: " + e.getMessage());
41+
}
42+
}
43+
44+
/**
45+
* CSV 한 줄을 안전하게 split하는 메서드
46+
* (문장 안에 쉼표가 포함된 경우 대응)
47+
*/
48+
private static String[] splitCsvLine(String line) {
49+
// 따옴표를 기준으로 나누되, 따옴표 안의 콤마는 무시
50+
return line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);
51+
}
52+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.back.global.standard.util;
2+
3+
public class DecimalToFractionConverter {
4+
// ingredient 문자열 전체를 처리 (예: "진:4.5 cl, 레몬 주스:1.5 cl")
5+
public static String convert(String value) {
6+
if (value == null || value.isBlank()) return value;
7+
8+
// 쉼표로 구분된 각 재료별로 처리
9+
String[] items = value.split(",");
10+
for (int i = 0; i < items.length; i++) {
11+
items[i] = convertSingleItem(items[i].trim());
12+
}
13+
return String.join(", ", items);
14+
}
15+
16+
// 단일 재료 문자열 처리
17+
private static String convertSingleItem(String item) {
18+
try {
19+
// 숫자만 추출 (정수+소수)
20+
String numericPart = item.replaceAll("[^0-9.]", "");
21+
if (numericPart.isEmpty()) return item;
22+
23+
double number = Double.parseDouble(numericPart);
24+
int intPart = (int) number;
25+
double fracPart = number - intPart;
26+
double tolerance = 0.02;
27+
28+
String fraction = "";
29+
30+
// 소수 -> 분수 변환
31+
if (Math.abs(fracPart - 0.25) < tolerance) fraction = "1/4";
32+
else if (Math.abs(fracPart - 0.33) < tolerance || Math.abs(fracPart - 0.3333) < tolerance)
33+
fraction = "1/3";
34+
else if (Math.abs(fracPart - 0.5) < tolerance) fraction = "1/2";
35+
else if (Math.abs(fracPart - 0.66) < tolerance || Math.abs(fracPart - 0.6666) < tolerance)
36+
fraction = "2/3";
37+
else if (Math.abs(fracPart - 0.75) < tolerance) fraction = "3/4";
38+
39+
// fraction이 있으면 int + fraction, 없으면 정수면 int만, 소수면 그대로
40+
String fractionStr;
41+
if (!fraction.isEmpty()) {
42+
fractionStr = intPart == 0 ? fraction : intPart + " " + fraction;
43+
} else {
44+
fractionStr = (number == intPart) ? String.valueOf(intPart) : String.valueOf(number);
45+
}
46+
47+
// 원본 item에서 숫자 부분만 교체
48+
return item.replace(numericPart, fractionStr);
49+
50+
} catch (NumberFormatException e) {
51+
return item; // 이미 분수 등 숫자가 아닌 경우 그대로 반환
52+
}
53+
}
54+
55+
// 테스트용 main
56+
public static void main(String[] args) {
57+
String test = "진:4.5 cl, 레몬 주스:1.5 cl, 마라스키노 리큐르:1.5 cl, 설탕:1.0 cl, 물:2 cl";
58+
System.out.println("변환 전: " + test);
59+
System.out.println("변환 후: " + convert(test));
60+
61+
String[] testValues = {"4.5", "1.5", "0.5", "2.25", "3.75", "1 1/2", "abc", "2.0"};
62+
for (String val : testValues) {
63+
System.out.printf("%s → %s%n", val, convert(val));
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)