Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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