Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ terraform/secrets.tf
### Claude AI ###
CLAUDE.md
.claude/

## dataSet ##
cocktails_ver00.csv
cocktails_ver01.csv
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
implementation("org.springframework.boot:spring-boot-starter-batch")
testImplementation("org.springframework.batch:spring-batch-test")

runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
compileOnly("org.projectlombok:lombok")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
@Repository
public interface CocktailCommentRepository extends JpaRepository<CocktailComment, Long> {

List<CocktailComment> findTop10ByCocktailIdOrderByIdDesc(Long cocktailId);
List<CocktailComment> findTop10ByCocktailIdAndStatusInOrderByIdDesc(
Long cocktailId, List<CommentStatus> statuses
);

List<CocktailComment> findTop10ByCocktailIdAndIdLessThanOrderByIdDesc(Long cocktailId, Long lastId);
List<CocktailComment> findTop10ByCocktailIdAndStatusInAndIdLessThanOrderByIdDesc(
Long cocktailId, List<CommentStatus> statuses, Long lastId
);

boolean existsByCocktailIdAndUserIdAndStatusNot(Long cocktailId, Long id, CommentStatus status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.back.domain.cocktail.repository.CocktailRepository;
import com.back.domain.post.comment.enums.CommentStatus;
import com.back.domain.user.entity.User;
import com.back.global.exception.UnauthorizedException;
import com.back.global.rq.Rq;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -46,6 +47,7 @@ public CocktailCommentResponseDto createCocktailComment(Long cocktailId, Cocktai
.cocktail(cocktail)
.user(user)
.content(reqBody.content())
.status(reqBody.status())
.build();

return new CocktailCommentResponseDto(cocktailCommentRepository.save(cocktailComment));
Expand All @@ -54,17 +56,34 @@ public CocktailCommentResponseDto createCocktailComment(Long cocktailId, Cocktai
// 칵테일 댓글 다건 조회 로직 (무한스크롤)
@Transactional(readOnly = true)
public List<CocktailCommentResponseDto> getCocktailComments(Long cocktailId, Long lastId) {
User actor = rq.getActor(); // 서비스에서 호출 가능

if (actor == null) {
throw new UnauthorizedException("로그인이 필요합니다.");
}
Long currentUserId = actor.getId();
List<CocktailComment> comments;

if (lastId == null) {
return cocktailCommentRepository.findTop10ByCocktailIdOrderByIdDesc(cocktailId)
.stream()
.map(CocktailCommentResponseDto::new)
.toList();
comments = cocktailCommentRepository
.findTop10ByCocktailIdAndStatusInOrderByIdDesc(cocktailId, List.of(CommentStatus.PUBLIC, CommentStatus.PRIVATE)
);
} else {
return cocktailCommentRepository.findTop10ByCocktailIdAndIdLessThanOrderByIdDesc(cocktailId, lastId)
.stream()
.map(CocktailCommentResponseDto::new)
.toList();
comments = cocktailCommentRepository
.findTop10ByCocktailIdAndStatusInAndIdLessThanOrderByIdDesc(cocktailId, List.of(CommentStatus.PUBLIC, CommentStatus.PRIVATE),
lastId);
}

return comments.stream()
.filter(comment ->{
if(comment.getStatus() == CommentStatus.PUBLIC) return true;
if(comment.getStatus() == CommentStatus.PRIVATE) {
return comment.getUser().getId().equals(currentUserId);
}
return false;
})
.map(CocktailCommentResponseDto::new)
.toList();
}

// 칵테일 댓글 단건 조회 로직
Expand Down
25 changes: 14 additions & 11 deletions src/main/java/com/back/domain/cocktail/service/CocktailService.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,19 @@ private String convertFractions(String ingredient) {
if (ingredient == null) return null;

// 치환 테이블 생성
Map<String, String> fractionMap = Map.of(
"1/2", "½",
"1/3", "⅓",
"2/3", "⅔",
"1/4", "¼",
"3/4", "¾",
"1/8", "⅛",
"3/8", "⅜",
"5/8", "⅝",
"7/8", "⅞"
Map<String, String> fractionMap = Map.ofEntries(
Map.entry("1/2", "½"),
Map.entry("1/3", "⅓"),
Map.entry("2/3", "⅔"),
Map.entry("1/4", "¼"),
Map.entry("3/4", "¾"),
Map.entry("1/8", "⅛"),
Map.entry("3/8", "⅜"),
Map.entry("5/8", "⅝"),
Map.entry("7/8", "⅞"),
Map.entry("1/5", "⅕"),
Map.entry("2/5", "⅖"),
Map.entry("1/6", "⅙")
);

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

// (숫자 + 선택적 분수) + (공백) + (단위)
Pattern pattern = Pattern.compile(
"^([0-9]*\\s*[½⅓⅔¼¾⅛⅜⅝⅞]?)\\s*(.*)$",
"^([0-9]*\\s*[½⅓⅔¼¾⅛⅜⅝⅞⅕⅖⅙]?)\\s*(.*)$",
Pattern.UNICODE_CHARACTER_CLASS
);
Matcher matcher = pattern.matcher(amountUnit);
Expand Down
96 changes: 96 additions & 0 deletions src/main/java/com/back/global/appConfig/CocktailBatchConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.back.global.appConfig;

import com.back.global.standard.util.DecimalToFractionConverter;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;
import org.springframework.batch.item.file.mapping.PassThroughLineMapper;
import org.springframework.batch.item.file.transform.PassThroughLineAggregator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.transaction.PlatformTransactionManager;

// DecimalToFractionConverter가 있다고 가정합니다.
// import com.example.DecimalToFractionConverter;

@Configuration
@EnableBatchProcessing
@RequiredArgsConstructor
public class CocktailBatchConfig {

private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;

private static final String INPUT_PATH = "src/main/resources/cocktails.csv";
private static final String OUTPUT_PATH = "src/main/resources/cocktails_clean.csv";

// 1.Reader: CSV 파일 한 줄씩 읽기
@Bean
public FlatFileItemReader<String> reader() {
return new FlatFileItemReaderBuilder<String>()
.name("cocktailReader")
.resource(new FileSystemResource(INPUT_PATH))
.lineMapper(new PassThroughLineMapper())
.build();
}

// 2️⃣ Processor: ingredient 컬럼(6번째)만 DecimalToFractionConverter로 변환
@Bean
public ItemProcessor<String, String> processor() {
return line -> {
// CSV 한 줄을 안전하게 split
String[] columns = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);

// Header인지 확인: "ingredient"라는 컬럼 이름으로 간단 체크
if (columns.length > 6 && !columns[6].equalsIgnoreCase("ingredient")) {
// DecimalToFractionConverter는 별도로 구현되어 있어야 합니다.
columns[6] = DecimalToFractionConverter.convert(columns[6]);
}

// 다시 CSV 문자열로 합치기
return String.join(",", columns);
};
}

// 3️⃣ Writer: 변환된 CSV 출력 (변화 없음)
@Bean
public FlatFileItemWriter<String> writer() {
return new FlatFileItemWriterBuilder<String>()
.name("cocktailWriter")
.resource(new FileSystemResource(OUTPUT_PATH))
.lineAggregator(new PassThroughLineAggregator<>())
.build();
}

// 4️⃣ Step: Chunk 단위 처리 (StepBuilderFactory 대체)
@Bean
public Step convertStep() {
// StepBuilder를 직접 생성하고, jobRepository와 transactionManager를 주입합니다.
return new StepBuilder("convertStep", jobRepository)
// chunk 메서드에 transactionManager를 인수로 전달합니다.
.<String, String>chunk(5, transactionManager)
.reader(reader())
.processor(processor())
.writer(writer())
.build();
}

// 5️⃣ Job 정의 (JobBuilderFactory 대체)
@Bean
public Job convertJob() {
// JobBuilder를 직접 생성하고, jobRepository를 주입합니다.
return new JobBuilder("convertJob", jobRepository)
.start(convertStep())
.build();
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/back/global/exception/UnauthorizedException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.back.global.exception;

public class UnauthorizedException extends ServiceException {

private static final int UNAUTHORIZED_CODE = 401;
private static final String UNAUTHORIZED_MSG = "로그인이 필요합니다.";

public UnauthorizedException() {
super(UNAUTHORIZED_CODE, UNAUTHORIZED_MSG);
}

public UnauthorizedException(String msg) {
super(UNAUTHORIZED_CODE, msg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.back.global.standard.util;

import java.io.*;

public class CocktailDataCleaner {
public static void main(String[] args) {
String inputPath = "src/main/resources/cocktails.csv"; // 원본 파일 경로
String outputPath = "src/main/resources/cocktails_clean.csv"; // 정제 후 파일 경로

try (
BufferedReader br = new BufferedReader(new FileReader(inputPath));
BufferedWriter bw = new BufferedWriter(new FileWriter(outputPath))
) {
String line;
boolean isHeader = true;

while ((line = br.readLine()) != null) {
// CSV 안의 쉼표와 큰따옴표를 안전하게 처리
String[] columns = splitCsvLine(line);

if (isHeader) {
bw.write(String.join(",", columns));
bw.newLine();
isHeader = false;
continue;
}

// ✅ 6번째 인덱스(ingredient) 컬럼만 변환
if (columns.length > 6) {
columns[6] = DecimalToFractionConverter.convert(columns[6]);
}

bw.write(String.join(",", columns));
bw.newLine();
}

System.out.println("✅ 칵테일 데이터 정제가 완료되었습니다: " + outputPath);

} catch (IOException e) {
System.err.println("❌ 파일 처리 중 오류 발생: " + e.getMessage());
}
}

/**
* CSV 한 줄을 안전하게 split하는 메서드
* (문장 안에 쉼표가 포함된 경우 대응)
*/
private static String[] splitCsvLine(String line) {
// 따옴표를 기준으로 나누되, 따옴표 안의 콤마는 무시
return line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.back.global.standard.util;

public class DecimalToFractionConverter {
// ingredient 문자열 전체를 처리 (예: "진:4.5 cl, 레몬 주스:1.5 cl")
public static String convert(String value) {
if (value == null || value.isBlank()) return value;

// 쉼표로 구분된 각 재료별로 처리
String[] items = value.split(",");
for (int i = 0; i < items.length; i++) {
items[i] = convertSingleItem(items[i].trim());
}
return String.join(", ", items);
}

// 단일 재료 문자열 처리
private static String convertSingleItem(String item) {
try {
// 숫자만 추출 (정수+소수)
String numericPart = item.replaceAll("[^0-9.]", "");
if (numericPart.isEmpty()) return item;

double number = Double.parseDouble(numericPart);
int intPart = (int) number;
double fracPart = number - intPart;
double tolerance = 0.02;

String fraction = "";

// 소수 -> 분수 변환
if (Math.abs(fracPart - 0.25) < tolerance) fraction = "1/4";
else if (Math.abs(fracPart - 0.33) < tolerance || Math.abs(fracPart - 0.3333) < tolerance)
fraction = "1/3";
else if (Math.abs(fracPart - 0.5) < tolerance) fraction = "1/2";
else if (Math.abs(fracPart - 0.66) < tolerance || Math.abs(fracPart - 0.6666) < tolerance)
fraction = "2/3";
else if (Math.abs(fracPart - 0.75) < tolerance) fraction = "3/4";

// fraction이 있으면 int + fraction, 없으면 정수면 int만, 소수면 그대로
String fractionStr;
if (!fraction.isEmpty()) {
fractionStr = intPart == 0 ? fraction : intPart + " " + fraction;
} else {
fractionStr = (number == intPart) ? String.valueOf(intPart) : String.valueOf(number);
}

// 원본 item에서 숫자 부분만 교체
return item.replace(numericPart, fractionStr);

} catch (NumberFormatException e) {
return item; // 이미 분수 등 숫자가 아닌 경우 그대로 반환
}
}

// 테스트용 main
public static void main(String[] args) {
String test = "진:4.5 cl, 레몬 주스:1.5 cl, 마라스키노 리큐르:1.5 cl, 설탕:1.0 cl, 물:2 cl";
System.out.println("변환 전: " + test);
System.out.println("변환 후: " + convert(test));

String[] testValues = {"4.5", "1.5", "0.5", "2.25", "3.75", "1 1/2", "abc", "2.0"};
for (String val : testValues) {
System.out.printf("%s → %s%n", val, convert(val));
}
}
}
Loading