Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
13 changes: 13 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: PR Agent
on:
pull_request:
types: [opened, synchronize]
jobs:
pr_agent_job:
runs-on: ubuntu-latest
steps:
- name: PR Agent action step
uses: Codium-ai/pr-agent@main
Comment on lines +9 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

์•ก์…˜ ๋ฒ„์ „์„ ํŠน์ • ์ปค๋ฐ‹ SHA๋กœ ๊ณ ์ •ํ•˜์„ธ์š”.

@main ๋ธŒ๋žœ์น˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•ญ์ƒ ์ตœ์‹  ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด ๊ณต๊ธ‰๋ง ๋ณด์•ˆ ์œ„ํ—˜์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. GitHub Actions ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€์— ๋”ฐ๋ผ third-party ์•ก์…˜์€ ํŠน์ • ์ปค๋ฐ‹ SHA๋กœ ๊ณ ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ถŒ์žฅ ์ˆ˜์ • ์‚ฌํ•ญ
-      - name: PR Agent action step
-        uses: Codium-ai/pr-agent@main
+      - name: PR Agent action step
+        uses: Codium-ai/pr-agent@<specific-commit-sha>

์ตœ์‹  ๋ฆด๋ฆฌ์Šค์˜ ์ปค๋ฐ‹ SHA๋ฅผ ํ™•์ธํ•˜๋ ค๋ฉด ๋‹ค์Œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”:

#!/bin/bash
# Codium-ai/pr-agent ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์˜ ์ตœ์‹  ๋ฆด๋ฆฌ์Šค ํƒœ๊ทธ ํ™•์ธ
gh api repos/Codium-ai/pr-agent/releases/latest --jq '.tag_name'

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
.github/workflows/main.yml lines 9-10: the workflow currently uses the
third-party action via "uses: Codium-ai/pr-agent@main"; replace the branch ref
with a specific commit SHA to pin the action and mitigate supply-chain risk.
Find the commit SHA for the desired release (via the repo's latest release or
GitHub UI), then update the uses line to "uses: Codium-ai/pr-agent@<COMMIT_SHA>"
so the workflow always runs that exact commit.

env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.G_TOKEN }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ ์ „์ฒด ๋‚ด์šฉ ํ™•์ธ
cat -n .github/workflows/main.yml

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 487


๐Ÿ Script executed:

# ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ G_TOKEN ์ฐธ์กฐ ๊ฒ€์ƒ‰
rg "G_TOKEN" --type yaml --type md

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


๐Ÿ Script executed:

# ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ์—์„œ GITHUB_TOKEN ์‚ฌ์šฉ ํŒจํ„ด ํ™•์ธ
rg "GITHUB_TOKEN|secrets\." .github/workflows/ -A 2 -B 2

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 341


๐ŸŒ Web query:

Codium-ai pr-agent GitHub GITHUB_TOKEN environment variable requirements documentation

๐Ÿ’ก Result:

Summary โ€” GITHUB_TOKEN for Codiumโ€‘AI / pr-agent (GitHub Action)

  • pr-agent expects a GitHub token (you can use the automatically-created secrets.GITHUB_TOKEN) to call the GitHub API. [1][2]
  • Recommended job permissions in workflows depend on the features you enable; typical minimal permissions are:
    • contents: read (to read repo files) and
    • pull-requests: write (to post reviews/comments) โ€” or issues: write for issue-based workflows. Set these with the workflow permissions key. [2][1][3]
  • If your workflow needs actions that GITHUB_TOKEN cannot grant (or your organization restricts GITHUB_TOKEN write access), use a PAT or a GitHub App installation token with the appropriate repo scopes. [3][1]

Sources

  • pr-agent GitHub / Qodo Merge docs (Run as GitHub Action). [1]
  • pr-agent "Help Docs" example workflow showing env and permissions (contents read, issues/pull-requests write). [2]
  • GitHub Actions docs on GITHUB_TOKEN and configuring permissions (how to change scopes, when to use PAT/GitHub App). [3]

๐Ÿ Script executed:

# ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ์˜ ์ „์ฒด ๊ตฌ์กฐ ํ™•์ธ (์ˆจ๊ฒจ์ง„ ๋ถ€๋ถ„์ด ์žˆ๋Š”์ง€ ํ™•์ธ)
wc -l .github/workflows/main.yml

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 117


secrets.GITHUB_TOKEN ์‚ฌ์šฉ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์„ธ์š”.

Codium-ai/pr-agent๋Š” ์ž๋™์œผ๋กœ ์ œ๊ณต๋˜๋Š” secrets.GITHUB_TOKEN์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ปค์Šคํ…€ ์‹œํฌ๋ฆฟ G_TOKEN์€ ๋ถˆํ•„์š”ํ•˜๋ฉฐ, ํ‘œ์ค€ GitHub ํ† ํฐ์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค:

๊ถŒ์žฅ ์ˆ˜์ •
         env:
           OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
-          GITHUB_TOKEN: ${{ secrets.G_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
GITHUB_TOKEN: ${{ secrets.G_TOKEN }}
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
๐Ÿค– Prompt for AI Agents
In .github/workflows/main.yml around line 13 the workflow sets GITHUB_TOKEN from
the custom secret secrets.G_TOKEN; replace that reference with the built-in
secrets.GITHUB_TOKEN so the workflow uses the automatic GitHub token, and
update/remove any other occurrences of G_TOKEN in the workflow file to avoid
references to the custom secret.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.loopers.domain.like.LikeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
* packageName : com.loopers.application.like
Expand All @@ -18,7 +17,6 @@
*/
@Component
@RequiredArgsConstructor
@Transactional
public class LikeFacade {

private final LikeService likeService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public class BrandService {

private final BrandRepository brandRepository;

@Transactional
public void save(Brand brand) {
brandRepository.save(brand);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static Order create(String userId) {
}

public void addOrderItem(OrderItem orderItem) {
orderItem.setOrder(this);
this.orderItems.add(orderItem);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public void charge(Long chargeAmount) {
throw new CoreException(ErrorType.BAD_REQUEST, "0์› ์ดํ•˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
this.balance += chargeAmount;
new Point(this.userId, this.balance);
}

public void use(Long useAmount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ public Point usePoint(String userId, Long useAmount) {
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));

if (useAmount == null || useAmount <= 0) {
throw new CoreException(ErrorType.NOT_FOUND, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
throw new CoreException(ErrorType.BAD_REQUEST, "์ฐจ๊ฐํ•  ํฌ์ธํŠธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
}

if (point.getBalance() < useAmount) {
throw new CoreException(ErrorType.NOT_FOUND, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.");
throw new CoreException(ErrorType.BAD_REQUEST, "ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.");
}

point.use(useAmount);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,17 @@ private Long requireValidPrice(Long price) {
return price;
}

public Long requireValidLikeCount(Long likeCount) {
private Long requireValidLikeCount(Long likeCount) {
if (likeCount == null || likeCount < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "์ข‹์•„์š” ๊ฐœ์ˆ˜๋Š” 0๊ฐœ ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
return likeCount;
}

private Long requireValidStock(Long stock) {
if (stock == null) {
throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.");
}
if (stock < 0) {
throw new CoreException(ErrorType.BAD_REQUEST, "์ƒํ’ˆ ์žฌ๊ณ ๋Š” 0 ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
* packageName : com.loopers.domain.product
Expand All @@ -27,6 +28,7 @@ public class ProductDomainService {
private final BrandRepository brandRepository;
private final LikeRepository likeRepository;

@Transactional(readOnly = true)
public ProductDetail getProductDetail(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class UserService {
@Transactional
public User register(String userId, String email, String birth, String gender) {
userRepository.findByUserId(userId).ifPresent(user -> {
throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์žID ์ž…๋‹ˆ๋‹ค.");
throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž ID ์ž…๋‹ˆ๋‹ค.");
});

User user = new User(userId, email, birth, gender);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import com.loopers.domain.brand.Brand;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

/**
* packageName : com.loopers.infrastructure.brand
* fileName : BrandJpaRepository
Expand All @@ -17,5 +15,4 @@
* 2025. 11. 12. byeonsungmun ์ตœ์ดˆ ์ƒ์„ฑ
*/
public interface BrandJpaRepository extends JpaRepository<Brand, Long> {
Optional<Brand> findById(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductRepository;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -38,13 +40,15 @@ public Optional<Product> findById(Long id) {

@Override
public void incrementLikeCount(Long productId) {
Product product = productJpaRepository.findById(productId).get();
Product product = productJpaRepository.findById(productId)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));
product.increaseLikeCount();
}

@Override
public void decrementLikeCount(Long productId) {
Product product = productJpaRepository.findById(productId).get();
Product product = productJpaRepository.findById(productId)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "์ƒํ’ˆ์„ ์ฐพ์„์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));
product.decreaseLikeCount();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public Optional<User> findByUserId(String userId) {

@Override
public User save(User user) {
userJpaRepository.save(user);
return user;
return userJpaRepository.save(user);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ApiResponse<PointV1Dto.PointResponse> getPoint(
description = "ํšŒ์›์˜ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ํ•œ๋‹ค."
)
ApiResponse<PointV1Dto.PointResponse> chargePoint(
@Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์กฐํšŒํ•  ํšŒ์› ID")
@Schema(name = "ํฌ์ธํŠธ ์ถฉ์ „ ์š”์ฒญ", description = "์ถฉ์ „ํ•  ํฌ์ธํŠธ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ")
PointV1Dto.ChargePointRequest request
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ void throwsException_whenInvalidEmailFormat() {
void throwsException_whenInvalidBirthFormat() {
// given
String userId = "yh45g";
String invalidEmail = "invalid-email-format"; // '@' ์—†์Œ
String birth = "1994-12-05";
String email = "valid@loopers.com";
String invalidBirth = "19941205"; // ํ˜•์‹ ์˜ค๋ฅ˜: ํ•˜์ดํ”ˆ ์—†์Œ
String gender = "MALE";

// when & then
assertThrows(CoreException.class, () -> new User(userId, invalidEmail, birth, gender));
assertThrows(CoreException.class, () -> new User(userId, email, invalidBirth, gender));
}
}
}
21 changes: 21 additions & 0 deletions apps/commerce-batch/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
dependencies {
// add-ons
implementation(project(":modules:jpa"))
implementation(project(":modules:redis"))
implementation(project(":supports:jackson"))
implementation(project(":supports:logging"))
implementation(project(":supports:monitoring"))

// batch
implementation("org.springframework.boot:spring-boot-starter-batch")
testImplementation("org.springframework.batch:spring-batch-test")

// querydsl
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")

// test-fixtures
testImplementation(testFixtures(project(":modules:jpa")))
testImplementation(testFixtures(project(":modules:redis")))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.loopers;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

import java.util.TimeZone;

@ConfigurationPropertiesScan
@SpringBootApplication
public class CommerceBatchApplication {

@PostConstruct
public void started() {
// set timezone
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
}

public static void main(String[] args) {
int exitCode = SpringApplication.exit(SpringApplication.run(CommerceBatchApplication.class, args));
System.exit(exitCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.loopers.batch.job.demo;

import com.loopers.batch.job.demo.step.DemoTasklet;
import com.loopers.batch.listener.JobListener;
import com.loopers.batch.listener.StepMonitorListener;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME)
@RequiredArgsConstructor
@Configuration
public class DemoJobConfig {
public static final String JOB_NAME = "demoJob";
private static final String STEP_DEMO_SIMPLE_TASK_NAME = "demoSimpleTask";

private final JobRepository jobRepository;
private final JobListener jobListener;
private final StepMonitorListener stepMonitorListener;
private final DemoTasklet demoTasklet;

@Bean(JOB_NAME)
public Job demoJob() {
return new JobBuilder(JOB_NAME, jobRepository)
.incrementer(new RunIdIncrementer())
.start(categorySyncStep())
.listener(jobListener)
.build();
}

@JobScope
@Bean(STEP_DEMO_SIMPLE_TASK_NAME)
public Step categorySyncStep() {
return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository)
.tasklet(demoTasklet, new ResourcelessTransactionManager())
.listener(stepMonitorListener)
.build();
}
Comment on lines +40 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ› ๏ธ Refactor suggestion | ๐ŸŸ  Major

๋ฉ”์„œ๋“œ๋ช…๊ณผ ์Šคํ…๋ช…์˜ ๋ถˆ์ผ์น˜๋ฅผ ์ˆ˜์ •ํ•˜์„ธ์š”.

๋ฉ”์„œ๋“œ๋ช… categorySyncStep()์€ ์นดํ…Œ๊ณ ๋ฆฌ ๋™๊ธฐํ™”๋ฅผ ์•”์‹œํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” STEP_DEMO_SIMPLE_TASK_NAME ("demoSimpleTask")์ด๋ผ๋Š” ์ด๋ฆ„์˜ ๋ฐ๋ชจ ์Šคํ…์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ถˆ์ผ์น˜๋Š” ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ ํ•ด์น˜๊ณ  ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
     @JobScope
     @Bean(STEP_DEMO_SIMPLE_TASK_NAME)
-    public Step categorySyncStep() {
+    public Step demoSimpleTaskStep() {
         return new StepBuilder(STEP_DEMO_SIMPLE_TASK_NAME, jobRepository)
                 .tasklet(demoTasklet, new ResourcelessTransactionManager())
                 .listener(stepMonitorListener)
                 .build();
     }

๊ทธ๋ฆฌ๊ณ  Job ๋นˆ์—์„œ๋„ ๋ฉ”์„œ๋“œ๋ช…์„ ์—…๋ฐ์ดํŠธํ•˜์„ธ์š”:

     @Bean(JOB_NAME)
     public Job demoJob() {
         return new JobBuilder(JOB_NAME, jobRepository)
                 .incrementer(new RunIdIncrementer())
-                .start(categorySyncStep())
+                .start(demoSimpleTaskStep())
                 .listener(jobListener)
                 .build();
     }

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/job/demo/DemoJobConfig.java
around lines 40 to 47, the method name categorySyncStep() does not match the
step name STEP_DEMO_SIMPLE_TASK_NAME ("demoSimpleTask"); rename the method to
demoSimpleTaskStep (or another name that clearly matches the
STEP_DEMO_SIMPLE_TASK_NAME) and update any references (including the Job @Bean
method) to use the new method name so method names and step names are consistent
for readability and maintainability.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.loopers.batch.job.demo.step;

import com.loopers.batch.job.demo.DemoJobConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@StepScope
@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = DemoJobConfig.JOB_NAME)
@RequiredArgsConstructor
@Component
public class DemoTasklet implements Tasklet {
@Value("#{jobParameters['requestDate']}")
private String requestDate;

@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
if (requestDate == null) {
throw new RuntimeException("requestDate is null");
}
System.out.println("Demo Tasklet ์‹คํ–‰ (์‹คํ–‰ ์ผ์ž : " + requestDate + ")");
Thread.sleep(1000);
System.out.println("Demo Tasklet ์ž‘์—… ์™„๋ฃŒ");
return RepeatStatus.FINISHED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.loopers.batch.listener;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.annotation.AfterChunk;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.stereotype.Component;

@Slf4j
@RequiredArgsConstructor
@Component
public class ChunkListener {

@AfterChunk
void afterChunk(ChunkContext chunkContext) {
log.info(
"์ฒญํฌ ์ข…๋ฃŒ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
"writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
);
Comment on lines +16 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

Java๋Š” ๋ฌธ์ž์—ด ๋ณด๊ฐ„์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ ๋ฉ”์‹œ์ง€์—์„œ ${} ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€๋งŒ, Java๋Š” ์ด๋Ÿฌํ•œ ๋ฌธ์ž์—ด ๋ณด๊ฐ„(string interpolation)์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ๊ฐ’์ด ์ถœ๋ ฅ๋˜์ง€ ์•Š๊ณ  "${chunkContext.stepContext..." ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋กœ๊ทธ์— ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์˜ฌ๋ฐ”๋ฅธ ๋กœ๊น… ๋ฐฉ์‹

SLF4J์˜ ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ๋กœ๊น…์„ ์‚ฌ์šฉํ•˜์„ธ์š”:

     log.info(
-        "์ฒญํฌ ์ข…๋ฃŒ: readCount: ${chunkContext.stepContext.stepExecution.readCount}, " +
-                "writeCount: ${chunkContext.stepContext.stepExecution.writeCount}"
+        "์ฒญํฌ ์ข…๋ฃŒ: readCount: {}, writeCount: {}",
+        chunkContext.getStepContext().getStepExecution().getReadCount(),
+        chunkContext.getStepContext().getStepExecution().getWriteCount()
     );
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/batch/listener/ChunkListener.java
around lines 16โ€“19, the log message currently uses Java-incompatible `${...}`
interpolation so the expressions are logged literally; replace the interpolation
with SLF4J parameterized logging by using {} placeholders and pass the actual
values as separate arguments (retrieve them with the appropriate getters, e.g.,
chunkContext.getStepContext().getStepExecution().getReadCount() and
getWriteCount()), ensuring you pass the values in the same order as the
placeholders.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.loopers.batch.listener;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.annotation.AfterJob;
import org.springframework.batch.core.annotation.BeforeJob;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;

@Slf4j
@RequiredArgsConstructor
@Component
public class JobListener {

@BeforeJob
void beforeJob(JobExecution jobExecution) {
log.info("Job '${jobExecution.jobInstance.jobName}' ์‹œ์ž‘");
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}
Comment on lines +19 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

Java๋Š” ๋ฌธ์ž์—ด ๋ณด๊ฐ„์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Line 21์˜ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€์—์„œ ${jobExecution.jobInstance.jobName} ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€๋งŒ, Java๋Š” ์ด๋Ÿฌํ•œ ๋ฌธ์ž์—ด ๋ณด๊ฐ„์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ œ Job ์ด๋ฆ„์ด ์ถœ๋ ฅ๋˜์ง€ ์•Š๊ณ  ๋ฆฌํ„ฐ๋Ÿด ๋ฌธ์ž์—ด์ด ๊ทธ๋Œ€๋กœ ๋กœ๊ทธ์— ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์˜ฌ๋ฐ”๋ฅธ ๋กœ๊น… ๋ฐฉ์‹

SLF4J์˜ ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ๋กœ๊น…์„ ์‚ฌ์šฉํ•˜์„ธ์š”:

-        log.info("Job '${jobExecution.jobInstance.jobName}' ์‹œ์ž‘");
+        log.info("Job '{}' ์‹œ์ž‘", jobExecution.getJobInstance().getJobName());
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@BeforeJob
void beforeJob(JobExecution jobExecution) {
log.info("Job '${jobExecution.jobInstance.jobName}' ์‹œ์ž‘");
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}
@BeforeJob
void beforeJob(JobExecution jobExecution) {
log.info("Job '{}' ์‹œ์ž‘", jobExecution.getJobInstance().getJobName());
jobExecution.getExecutionContext().putLong("startTime", System.currentTimeMillis());
}
๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/src/main/java/com/loopers/batch/listener/JobListener.java
around lines 19 to 23, the log uses a Java-incompatible string interpolation
("${...}") so the job name won't be printed; replace it with SLF4J parameterized
logging and the proper getter calls (e.g. log.info("Job '{}' ์‹œ์ž‘",
jobExecution.getJobInstance().getJobName());) and ensure you use
jobExecution.getJobInstance().getJobName() (or the correct accessor) rather than
an interpolated string.


@AfterJob
void afterJob(JobExecution jobExecution) {
var startTime = jobExecution.getExecutionContext().getLong("startTime");
var endTime = System.currentTimeMillis();

var startDateTime = Instant.ofEpochMilli(startTime)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
var endDateTime = Instant.ofEpochMilli(endTime)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();

var totalTime = endTime - startTime;
var duration = Duration.ofMillis(totalTime);
var hours = duration.toHours();
var minutes = duration.toMinutes() % 60;
var seconds = duration.getSeconds() % 60;

var message = String.format(
"""
*Start Time:* %s
*End Time:* %s
*Total Time:* %d์‹œ๊ฐ„ %d๋ถ„ %d์ดˆ
""", startDateTime, endDateTime, hours, minutes, seconds
).trim();

log.info(message);
}
}
Loading