Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
11 commits
Select commit Hold shift + click to select a range
fb4dc84
feat: ์˜ˆ์•ฝ ์Šฌ๋กฏ ์ž๋™ ์ƒ์„ฑ ์Šค์ผ€์ค„๋Ÿฌ ์ถ”๊ฐ€
johe00123 May 13, 2026
b003526
fix: ReservationService ์ถฉ๋Œ ํ•ด๊ฒฐ ์‹œ ๋ˆ„๋ฝ๋œ import ๋ฐ ๋ณ€์ˆ˜ ์ถ”๊ฐ€
johe00123 May 13, 2026
bc6812f
Merge branch 'develop' of https://github.com/catchtable-clone/backendโ€ฆ
johe00123 May 13, 2026
6b0a97b
Merge branch 'develop' of https://github.com/catchtable-clone/backendโ€ฆ
johe00123 May 14, 2026
bb9f8ab
Merge remote-tracking branch 'origin/develop' into feat/#61-slot-geneโ€ฆ
johe00123 May 14, 2026
584a79d
fix: ์Šฌ๋กฏ ์ž๋™ ์ƒ์„ฑ ์‹œ ๋ถ€๋ถ„ ์ƒ์„ฑ๋œ ์Šฌ๋กฏ ๋ˆ„๋ฝ ๋ฌธ์ œ ํ•ด๊ฒฐ
johe00123 May 15, 2026
44f49e6
refactor: ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๋‹จ์ผ ๋‚ ์งœ์—์„œ 30์ผ ๋ฒ”์œ„ ๋ฃจํ”„๋กœ ๋ณ€๊ฒฝ
johe00123 May 15, 2026
0e6d093
fix: ์Šฌ๋กฏ ์ž๋™ ์ƒ์„ฑ ์‹œ ๋ถ€๋ถ„ ์ƒ์„ฑ๋œ ์Šฌ๋กฏ ๋ˆ„๋ฝ ๋ฌธ์ œ ํ•ด๊ฒฐ
johe00123 May 15, 2026
c1e9328
chore: ์Šค์ผ€์ค„๋Ÿฌ ๋„์ž… ํ›„ ๋ฏธ์‚ฌ์šฉ ์ˆ˜๋™ ์—”๋“œํฌ์ธํŠธ ์ฃผ์„ ์ฒ˜๋ฆฌ
johe00123 May 15, 2026
222b5e6
refactor: PR #64 ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ˜์˜
johe00123 May 15, 2026
770140f
refactor: PR #65 ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ˜์˜ - ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ „์„ฑ, ํƒ€์ž… ์•ˆ์ „์„ฑ, ๋ฐ˜๋ณต ์—ฐ์‚ฐ ์ œ๊ฑฐ
johe00123 May 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ public class StoreRemainController {

private final StoreRemainService storeRemainService;

//๋‚˜์ค‘์— ์Šค์ผ€์ค„๋Ÿฌ
@PostMapping
public ResponseEntity<ApiResponse<Void>> generateMonthlyRemain(
@Valid @RequestBody StoreRemainCreateRequestDto request
) {
storeRemainService.generateMonthlyRemain(request);
return ResponseEntity
.status(SuccessCode.REMAIN_CREATE_SUCCESS.getHttpStatus())
.body(ApiResponse.success(SuccessCode.REMAIN_CREATE_SUCCESS));
}
// ์Šค์ผ€์ค„๋Ÿฌ ๋„์ž… ํ›„ ์‚ฌ์šฉ ์•ˆ ํ•จ. ๊ธฐ์กด swagger ์—์„œ ์ˆ˜๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋˜ POST /api/v1/remains ์ฃผ์„ ์ฒ˜๋ฆฌ.
// @PostMapping
// public ResponseEntity<ApiResponse<Void>> generateMonthlyRemain(
// @Valid @RequestBody StoreRemainCreateRequestDto request
// ) {
// storeRemainService.generateMonthlyRemain(request);
// return ResponseEntity
// .status(SuccessCode.REMAIN_CREATE_SUCCESS.getHttpStatus())
// .body(ApiResponse.success(SuccessCode.REMAIN_CREATE_SUCCESS));
// }
Comment on lines +24 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ์ฝ”๋“œ๋Š” ๊ฐ€๋…์„ฑ์„ ์ €ํ•ดํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ์„ ๋†’์ž…๋‹ˆ๋‹ค. ์Šค์ผ€์ค„๋Ÿฌ ๋„์ž…์œผ๋กœ ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ด๋ผ๋ฉด ์ฝ”๋“œ๋ฅผ ์ฃผ์„์œผ๋กœ ๋‚จ๊ฒจ๋‘๊ธฐ๋ณด๋‹ค ์‚ญ์ œํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๊ฒฝ์šฐ Git ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ํ†ตํ•ด ์–ธ์ œ๋“ ์ง€ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


@GetMapping
public ResponseEntity<ApiResponse<List<StoreRemainResponseDto>>> getStoreRemains(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.catchtable.remain.dto.projection;

import java.time.LocalTime;

/**
* ๋งค์žฅ๋ณ„ ์Šฌ๋กฏ ์‹œ๊ฐ„ ์กฐํšŒ์šฉ Interface Projection.
* (storeId, remainTime) ํŽ˜์–ด๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
*/
public interface StoreRemainTimeView {

Long getStoreId();

LocalTime getRemainTime();
}
8 changes: 7 additions & 1 deletion src/main/java/com/catchtable/remain/entity/StoreRemain.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "store_remain")
@Table(
name = "store_remain",
indexes = {
@Index(name = "idx_store_remain_date_store", columnList = "remain_date, store_id"),
@Index(name = "idx_store_remain_store_date", columnList = "store_id, remain_date")
}
)
public class StoreRemain {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.catchtable.remain.repository;

import com.catchtable.remain.dto.projection.StoreRemainTimeView;
import com.catchtable.remain.entity.StoreRemain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
Expand Down Expand Up @@ -31,4 +32,10 @@ Optional<StoreRemain> findByStoreNameAndDateTime(
@Param("date") LocalDate date,
@Param("time") LocalTime time
);

@Query("SELECT sr.store.id AS storeId, sr.remainTime AS remainTime FROM StoreRemain sr " +
"WHERE sr.remainDate = :date AND sr.store.id IN :storeIds")
List<StoreRemainTimeView> findStoreIdAndTimesByDateAndStoreIds(
@Param("date") LocalDate date,
@Param("storeIds") List<Long> storeIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.catchtable.remain.scheduler;

import com.catchtable.remain.service.StoreRemainService;
import com.catchtable.remain.service.StoreSlotPlan;
import com.catchtable.store.entity.Store;
import com.catchtable.store.repository.StoreRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.Clock;
import java.time.LocalDate;
import java.util.List;

/**
* ๋งค์ผ ์ƒˆ๋ฒฝ 4์‹œ์— "์˜ค๋Š˜ ํฌํ•จ 30์ผ" ๋ฒ”์œ„์˜ ์˜ˆ์•ฝ ์Šฌ๋กฏ์„ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค.
* ํ•ญ์ƒ ์˜ค๋Š˜๋ถ€ํ„ฐ 30์ผ์น˜ ์Šฌ๋กฏ์ด DB์— ์กด์žฌํ•˜๋„๋ก ๋ณด์ถฉ (์ฐจ์ง‘ํ•ฉ ๊ธฐ๋ฐ˜์ด๋ผ ์ด๋ฏธ ์žˆ๋Š” ๋‚ ์งœ๋Š” ์Šคํ‚ต).
* ๋งค์žฅ ๋ชฉ๋ก ์กฐํšŒ์™€ ์˜์—…์‹œ๊ฐ„ ํŒŒ์‹ฑ์€ ๋ฒ”์œ„ ๋ฃจํ”„ ์‹œ์ž‘ ์ „ ๋‹จ ํ•œ ๋ฒˆ๋งŒ ์ˆ˜ํ–‰ํ•ด
* 30ํšŒ ๋ฐ˜๋ณต ์กฐํšŒ/ํŒŒ์‹ฑ ๋น„์šฉ์„ ์ œ๊ฑฐํ•œ๋‹ค.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class StoreRemainGenerationScheduler {

private static final int RANGE_DAYS = 30;

private final StoreRemainService storeRemainService;
private final StoreRepository storeRepository;
private final Clock clock;

@Scheduled(cron = "0 0 4 * * *")
public void generateFutureSlots() {
List<Store> stores = storeRepository.findAllByIsDeletedFalse();
List<StoreSlotPlan> plans = storeRemainService.buildSlotPlans(stores);

LocalDate startDate = LocalDate.now(clock);
LocalDate endDate = startDate.plusDays(RANGE_DAYS - 1);

int totalFilledStores = 0;
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
totalFilledStores += storeRemainService.generateDailySlotsForPlans(plans, date);
}

if (totalFilledStores > 0) {
log.info("[์Šฌ๋กฏ ์ž๋™ ์ƒ์„ฑ] ๋ฒ”์œ„: {} ~ {}, ๋ณด์ถฉ ๋ฐœ์ƒ ๋งค์žฅ-์ผ ํ•ฉ๊ณ„: {}",
startDate, endDate, totalFilledStores);
}
}
}
186 changes: 156 additions & 30 deletions src/main/java/com/catchtable/remain/service/StoreRemainService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.catchtable.remain.dto.read.StoreRemainResponseDto;
import com.catchtable.remain.dto.create.StoreRemainCreateRequestDto;
import com.catchtable.remain.dto.projection.StoreRemainTimeView;
import com.catchtable.remain.entity.StoreRemain;
import com.catchtable.remain.repository.StoreRemainRepository;
import com.catchtable.global.exception.CustomException;
Expand All @@ -18,73 +19,62 @@
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

@Slf4j
@Service
@RequiredArgsConstructor
public class StoreRemainService {

private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
private static final int STORE_CHUNK_SIZE = 500;

private final StoreRemainRepository storeRemainRepository;
private final StoreRepository storeRepository;
private final StoreRemainSlotWriter storeRemainSlotWriter;

@Transactional
public void generateMonthlyRemain(StoreRemainCreateRequestDto request) {

Store store = storeRepository.findById(request.storeId())
.orElseThrow(() -> new CustomException(ErrorCode.STORE_NOT_FOUND));

// ๋งค์žฅ์˜ ๋ฌธ์ž์—ด ํ˜•ํƒœ("10:00") ์˜์—… ์‹œ๊ฐ„์„ LocalTime์œผ๋กœ ํŒŒ์‹ฑ
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
LocalTime openTime;
LocalTime closeTime;

try {
openTime = LocalTime.parse(store.getOpenTime(), formatter);
closeTime = LocalTime.parse(store.getCloseTime(), formatter);
openTime = LocalTime.parse(store.getOpenTime(), TIME_FORMATTER);
closeTime = LocalTime.parse(store.getCloseTime(), TIME_FORMATTER);
} catch (Exception e) {
log.error("๋งค์žฅ ์˜์—… ์‹œ๊ฐ„ ํŒŒ์‹ฑ ์‹คํŒจ. storeId: {}, openTime: {}, closeTime: {}",
log.error("๋งค์žฅ ์˜์—… ์‹œ๊ฐ„ ํŒŒ์‹ฑ ์‹คํŒจ. storeId: {}, openTime: {}, closeTime: {}",
store.getId(), store.getOpenTime(), store.getCloseTime());
throw new CustomException(ErrorCode.BAD_REQUEST);
}

// ํ•ด๋‹น ์›”์˜ 1์ผ๊ณผ ๋ง์ผ ๊ณ„์‚ฐ
YearMonth targetMonth = YearMonth.of(request.year(), request.month());
LocalDate startDate = targetMonth.atDay(1);
LocalDate endDate = targetMonth.atEndOfMonth();

List<LocalTime> slotTimes = buildSlotTimes(openTime, closeTime);
List<StoreRemain> remainsToSave = new ArrayList<>();

// 1์ผ๋ถ€ํ„ฐ ๋ง์ผ๊นŒ์ง€ ์ƒ์„ฑ
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {

LocalTime currentTime = openTime;

// ์‹œ๊ฐ„๋ณ„ ๋ฃจํ”„ (์˜คํ”ˆ ์‹œ๊ฐ„๋ถ€ํ„ฐ ๋งˆ๊ฐ ์‹œ๊ฐ„ 1์‹œ๊ฐ„ ์ „๊นŒ์ง€)
while (currentTime.isBefore(closeTime)) {

if (currentTime.plusHours(1).isAfter(closeTime)) {
break;
}

// ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ (์ž”์—ฌ ํŒ€ ์ˆ˜๋Š” ๋งค์žฅ์˜ team)
StoreRemain remain = StoreRemain.builder()
for (LocalTime time : slotTimes) {
remainsToSave.add(StoreRemain.builder()
.store(store)
.remainDate(date)
.remainTime(currentTime)
.remainTime(time)
.remainTeam(store.getTeam())
.build();

remainsToSave.add(remain);

currentTime = currentTime.plusHours(1);
.build());
}
}

// ์ €์žฅ
storeRemainRepository.saveAll(remainsToSave);

}

@Transactional(readOnly = true)
Expand All @@ -105,6 +95,142 @@ public List<StoreRemainResponseDto> getStoreRemains(Long storeId, LocalDate date
@Transactional(readOnly = true)
public Optional<StoreRemain> findAvailableRemain(String storeName, LocalDate date, LocalTime time) {
return storeRemainRepository.findByStoreNameAndDateTime(storeName, date, time)
.filter(remain -> remain.getRemainTeam() > 0); // ์ž”์—ฌ ํŒ€์ด 1 ์ด์ƒ์ธ ๊ฒฝ์šฐ๋งŒ ํ•„ํ„ฐ๋ง
.filter(remain -> remain.getRemainTeam() > 0);
}

/**
* ๋ชจ๋“  ํ™œ์„ฑ ๋งค์žฅ์— ๋Œ€ํ•ด ์ง€์ •๋œ ๋‚ ์งœ์˜ ์Šฌ๋กฏ์„ ์ƒ์„ฑํ•œ๋‹ค.
* ๋‹จ์ผ ํ˜ธ์ถœ ํŽธ์˜์šฉ - ๋งค์žฅ ๋ชฉ๋ก ์กฐํšŒ + ์˜์—…์‹œ๊ฐ„ ์‚ฌ์ „ ๊ณ„์‚ฐ ํ›„ generateDailySlotsForPlans์— ์œ„์ž„ํ•œ๋‹ค.
* 30์ผ ๊ฐ™์€ ๋ฒ”์œ„ ๋ฃจํ”„์—์„œ๋Š” buildSlotPlans๋ฅผ 1ํšŒ ํ˜ธ์ถœ ํ›„ generateDailySlotsForPlans๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๋ผ.
*
* @return ์Šฌ๋กฏ์ด ๋ณด์ถฉ๋œ ๋งค์žฅ ์ˆ˜
*/
@Transactional
public int generateDailySlotsForAllStores(LocalDate targetDate) {
List<Store> stores = storeRepository.findAllByIsDeletedFalse();
return generateDailySlotsForPlans(buildSlotPlans(stores), targetDate);
}

/**
* ๋งค์žฅ ๋ชฉ๋ก์„ ๋ฐ›์•„ ๋งค์žฅ๋ณ„ ์Šฌ๋กฏ ๊ณ„ํš(์˜์—…์‹œ๊ฐ„ ํŒŒ์‹ฑ + ์‹œ๊ฐ„ ์Šฌ๋กฏ ๋ชฉ๋ก)์„ 1ํšŒ ๊ณ„์‚ฐํ•œ๋‹ค.
* ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ 30์ผ ๋ฃจํ”„ ์‹œ์ž‘ ์ „ 1ํšŒ ํ˜ธ์ถœํ•˜๋ฉด ๋™์ผ ๋งค์žฅ์— ๋Œ€ํ•œ ๋ฐ˜๋ณต ํŒŒ์‹ฑ์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค.
* ์˜์—…์‹œ๊ฐ„ ํŒŒ์‹ฑ ์‹คํŒจ ๋งค์žฅ์€ ๊ฒฐ๊ณผ์—์„œ ์ œ์™ธํ•œ๋‹ค.
*/
public List<StoreSlotPlan> buildSlotPlans(List<Store> stores) {
List<StoreSlotPlan> plans = new ArrayList<>(stores.size());
for (Store store : stores) {
try {
LocalTime openTime = LocalTime.parse(store.getOpenTime(), TIME_FORMATTER);
LocalTime closeTime = LocalTime.parse(store.getCloseTime(), TIME_FORMATTER);
List<LocalTime> expectedTimes = buildSlotTimes(openTime, closeTime);
if (expectedTimes.isEmpty()) {
continue;
}
plans.add(new StoreSlotPlan(store, expectedTimes));
} catch (Exception e) {
log.error("[์˜์—…์‹œ๊ฐ„ ํŒŒ์‹ฑ ์‹คํŒจ] storeId={}, openTime={}, closeTime={}",
store.getId(), store.getOpenTime(), store.getCloseTime());
}
}
return plans;
}

/**
* ์‚ฌ์ „ ๊ณ„์‚ฐ๋œ ์Šฌ๋กฏ ๊ณ„ํš์— ๋”ฐ๋ผ ์ง€์ •๋œ ๋‚ ์งœ์˜ ์Šฌ๋กฏ์„ ์ƒ์„ฑํ•œ๋‹ค.
* - ๋งค์žฅ N๊ฐœ์”ฉ ์ฒญํฌ ๋‹จ์œ„๋กœ ๊ธฐ์กด ์Šฌ๋กฏ์„ ์กฐํšŒํ•˜๊ณ  ์ €์žฅํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ถ€๋‹ด ์ตœ์†Œํ™”
* - ์ฒญํฌ ๋‹จ์œ„ REQUIRES_NEW ํŠธ๋žœ์žญ์…˜์œผ๋กœ batch_size ํšจ๊ณผ + ์‹คํŒจ ๊ฒฉ๋ฆฌ ๋™์‹œ ํ™•๋ณด
* - ์˜์—…์‹œ๊ฐ„ ํŒŒ์‹ฑ/์Šฌ๋กฏ ๊ณ„์‚ฐ์€ buildSlotPlans์—์„œ ์ด๋ฏธ ์™„๋ฃŒ๋˜์–ด ๋ฃจํ”„๋งˆ๋‹ค ๋ฐ˜๋ณตํ•˜์ง€ ์•Š๋Š”๋‹ค.
*
* @return ์Šฌ๋กฏ์ด ๋ณด์ถฉ๋œ ๋งค์žฅ ์ˆ˜
*/
public int generateDailySlotsForPlans(List<StoreSlotPlan> plans, LocalDate targetDate) {
int filledStoreCount = 0;
for (int i = 0; i < plans.size(); i += STORE_CHUNK_SIZE) {
int end = Math.min(i + STORE_CHUNK_SIZE, plans.size());
List<StoreSlotPlan> chunk = plans.subList(i, end);

List<Long> chunkStoreIds = chunk.stream().map(p -> p.store().getId()).toList();
Map<Long, Set<LocalTime>> existingByStore = fetchExistingTimesByStores(chunkStoreIds, targetDate);

List<StoreRemain> chunkMissingSlots = new ArrayList<>();
int chunkFilledCount = 0;

for (StoreSlotPlan plan : chunk) {
List<StoreRemain> storeMissing = buildMissingSlotsForPlan(plan, targetDate, existingByStore);
if (!storeMissing.isEmpty()) {
chunkMissingSlots.addAll(storeMissing);
chunkFilledCount++;
}
}

if (chunkMissingSlots.isEmpty()) {
continue;
}

try {
storeRemainSlotWriter.saveSlots(chunkMissingSlots);
filledStoreCount += chunkFilledCount;
log.info("[์Šฌ๋กฏ ์ฒญํฌ ์ €์žฅ] targetDate={}, ๋งค์žฅ๋ฒ”์œ„=[{}~{}], ๋งค์žฅ์ˆ˜={}, ์Šฌ๋กฏ์ˆ˜={}",
targetDate, chunk.get(0).store().getId(), chunk.get(chunk.size() - 1).store().getId(),
chunkFilledCount, chunkMissingSlots.size());
} catch (Exception e) {
log.error("[์Šฌ๋กฏ ์ฒญํฌ ์ €์žฅ ์‹คํŒจ] targetDate={}, ๋งค์žฅ๋ฒ”์œ„=[{}~{}], error={}",
targetDate, chunk.get(0).store().getId(), chunk.get(chunk.size() - 1).store().getId(),
e.getMessage());
}
}
return filledStoreCount;
}

/**
* ์‚ฌ์ „ ๊ณ„์‚ฐ๋œ ๋งค์žฅ ์Šฌ๋กฏ ๊ณ„ํš์— ๋Œ€ํ•ด ๋ถ€์กฑํ•œ ์Šฌ๋กฏ ๋ชฉ๋ก์„ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๊ณ„์‚ฐํ•œ๋‹ค (DB ์ ‘๊ทผ ์—†์Œ).
*/
private List<StoreRemain> buildMissingSlotsForPlan(
StoreSlotPlan plan, LocalDate targetDate, Map<Long, Set<LocalTime>> existingByStore) {
Store store = plan.store();
Set<LocalTime> existingTimes = existingByStore.getOrDefault(store.getId(), Set.of());

List<StoreRemain> missingSlots = new ArrayList<>();
for (LocalTime time : plan.expectedTimes()) {
if (existingTimes.contains(time)) {
continue;
}
missingSlots.add(StoreRemain.builder()
.store(store)
.remainDate(targetDate)
.remainTime(time)
.remainTeam(store.getTeam())
.build());
}
return missingSlots;
}

/**
* ์ง€์ •๋œ ๋งค์žฅ ID ๋ชฉ๋ก๊ณผ ๋‚ ์งœ์— ํ•ด๋‹นํ•˜๋Š” ๊ธฐ์กด ์Šฌ๋กฏ ์‹œ๊ฐ„์„ ํ•œ ๋ฒˆ์— ์กฐํšŒํ•˜์—ฌ ๋งค์žฅ๋ณ„ Set์œผ๋กœ ๋ฌถ๋Š”๋‹ค.
* ์ฒญํฌ ๋ฒ”์œ„๋กœ ํ•œ์ •ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ๋งค์žฅ ์ „์ฒด๊ฐ€ ์•„๋‹Œ ์ฒญํฌ ํฌ๊ธฐ์— ๋น„๋ก€ํ•˜๋„๋ก ์ œํ•œํ•œ๋‹ค.
*/
private Map<Long, Set<LocalTime>> fetchExistingTimesByStores(List<Long> storeIds, LocalDate targetDate) {
Map<Long, Set<LocalTime>> map = new HashMap<>();
for (StoreRemainTimeView view : storeRemainRepository.findStoreIdAndTimesByDateAndStoreIds(targetDate, storeIds)) {
map.computeIfAbsent(view.getStoreId(), k -> new HashSet<>()).add(view.getRemainTime());
}
return map;
}

/**
* ์˜์—…์‹œ๊ฐ„ ๋ฒ”์œ„๋กœ 1์‹œ๊ฐ„ ๋‹จ์œ„ ์Šฌ๋กฏ ์‹œ๊ฐ„ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•œ๋‹ค.
* ๋งˆ์ง€๋ง‰ ์Šฌ๋กฏ์€ closeTime์„ ๋„˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค.
*/
private List<LocalTime> buildSlotTimes(LocalTime openTime, LocalTime closeTime) {
List<LocalTime> times = new ArrayList<>();
LocalTime current = openTime;
while (current.isBefore(closeTime)) {
if (current.plusHours(1).isAfter(closeTime)) {
break;
}
times.add(current);
current = current.plusHours(1);
}
return times;
}
}
}
Loading
Loading