Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c0f179f
chore: 개행 추가
dh2906 Nov 18, 2025
d6d84a4
refactor: 이너 클래스 명 변경
dh2906 Nov 18, 2025
19212ba
docs: 스웨거 명세 예시 추가
dh2906 Nov 18, 2025
d95df9f
refactor: 이름, 세부 이름 파싱 로직 분리
dh2906 Nov 18, 2025
0422d99
refactor: 문자열 분리 로직 모델에서 호출하도록 수정
dh2906 Nov 18, 2025
695adad
refactor: 파서 패키지 이동
dh2906 Nov 18, 2025
1ef2105
refactor: 엑셀 데이터 추출 클래스 명 Parser -> Extractor로 변경 및 패키지 이동
dh2906 Nov 18, 2025
a459143
refactor: 셀 내용 추출 클래스 이름 변경 및 메소드 명 변경
dh2906 Nov 18, 2025
1b2bab6
refactor: 파일 업로드 미리보기 메소드 이름 변경
dh2906 Nov 18, 2025
85e12c0
refactor: 엑셀 셀 문자열 추출을 PoiExtractor로 통일
dh2906 Nov 18, 2025
c5a1cb7
refactor: Extractor 클래스 stateful하도록 수정
dh2906 Nov 18, 2025
9c2a47f
refactor: PoiCellExtractor.extractStringValue() 호출하도록 수정
dh2906 Nov 18, 2025
d115bfe
refactor: 셀 내용 추출 시 서식에 대한 문제 방지
dh2906 Nov 18, 2025
3254c09
refactor: 응답 DTO 구조 변경
dh2906 Nov 18, 2025
414117e
refactor: 메소드 명 변경
dh2906 Nov 18, 2025
b886158
chore: 코드 포맷팅
dh2906 Nov 18, 2025
9bc0db6
refactor: 서비스 메소드 명 수정
dh2906 Nov 18, 2025
adbca27
fix: 업로드 API에서 요청 Content-Type을 파일로 지정
dh2906 Nov 24, 2025
eae0999
chore: EOF 추가
dh2906 Nov 26, 2025
e2533d9
fix: cell 값 추출 시 null-safe 처리하도록 수정
dh2906 Nov 26, 2025
dd6c17c
chore: 불필요한 개행 제거
dh2906 Nov 27, 2025
ce57dd8
Merge branch 'develop' into refactor/2090-shuttle-bus
dh2906 Nov 27, 2025
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 @@ -3,8 +3,7 @@
import static in.koreatech.koin.admin.history.enums.DomainType.SHUTTLE_BUS;
import static in.koreatech.koin.domain.user.model.UserType.ADMIN;
import static in.koreatech.koin.global.code.ApiResponseCode.*;

import java.util.List;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -36,8 +35,8 @@ public interface AdminShuttleBusTimetableApi {
})
@Operation(summary = "엑셀 파일을 업로드하여 파싱된 데이터를 미리보기 한다.")
@AdminActivityLogging(domain = SHUTTLE_BUS)
@PostMapping("/excel")
ResponseEntity<List<AdminShuttleBusTimetableResponse>> previewShuttleBusTimetable(
@PostMapping(value = "/excel", consumes = MULTIPART_FORM_DATA_VALUE)
ResponseEntity<AdminShuttleBusTimetableResponse> uploadTimetableExcelForPreview(
@Auth(permit = {ADMIN}) Integer adminId,
@RequestParam(name = "shuttle-bus-timetable") MultipartFile file
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import static in.koreatech.koin.admin.history.enums.DomainType.SHUTTLE_BUS;
import static in.koreatech.koin.domain.user.model.UserType.ADMIN;

import java.util.List;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -33,13 +32,12 @@ public class AdminShuttleBusTimetableController implements AdminShuttleBusTimeta
private final AdminShuttleBusService adminShuttleBusService;

@AdminActivityLogging(domain = SHUTTLE_BUS)
@PostMapping("/excel")
public ResponseEntity<List<AdminShuttleBusTimetableResponse>> previewShuttleBusTimetable(
@PostMapping(value = "/excel", consumes = MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<AdminShuttleBusTimetableResponse> uploadTimetableExcelForPreview(
@Auth(permit = {ADMIN}) Integer adminId,
@RequestParam(name = "shuttle-bus-timetable") MultipartFile file
) {
List<AdminShuttleBusTimetableResponse> response = adminShuttleBusExcelService
.previewShuttleBusTimetable(file);
AdminShuttleBusTimetableResponse response = adminShuttleBusExcelService.getShuttleBusTimetablePreview(file);

return ResponseEntity.ok(response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public List<ShuttleBusRoute.RouteInfo> toRouteInfoEntity() {
return routeInfo.stream()
.map(innerRouteInfoRequest ->
InnerRouteInfoRequest.toEntity(
innerRouteInfoRequest.name, innerRouteInfoRequest.detail, innerRouteInfoRequest.arrivalTime
innerRouteInfoRequest.name,
innerRouteInfoRequest.detail,
innerRouteInfoRequest.arrivalTime
)
).toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,84 @@
import lombok.Builder;

@JsonNaming(SnakeCaseStrategy.class)
@Builder
public record AdminShuttleBusTimetableResponse(
@Schema(description = "운행 지역", example = "CHEONAN_ASAN", requiredMode = REQUIRED)
String region,

@Schema(description = "노선 타입", example = "SHUTTLE", requiredMode = REQUIRED)
String routeType,

@Schema(description = "노선 이름", example = "천안 셔틀", requiredMode = REQUIRED)
String routeName,

@Schema(description = "노선 부제목", example = "토요일, 일요일", requiredMode = NOT_REQUIRED)
String subName,

@Schema(description = "정류소 정보 리스트")
List<NodeInfo> nodeInfo,

@Schema(description = "회차별 도착 시간 및 운행 요일 정보 리스트")
List<RouteInfo> routeInfo
@Schema(description = "셔틀 버스 시간표 정보 리스트")
List<InnerAdminShuttleBusTimetableResponse> shuttleBusTimetables
) {

public static AdminShuttleBusTimetableResponse from(ShuttleBusTimetable table) {
List<NodeInfo> nodeInfos = table.getNodeInfos().stream()
.map(n -> new AdminShuttleBusTimetableResponse.NodeInfo(
n.getName(),
n.getDetail()
))
.toList();

List<RouteInfo> routeInfos = table.getRouteInfos().stream()
.map(r -> new AdminShuttleBusTimetableResponse.RouteInfo(
r.getName(),
r.getDetail(),
r.getArrivalTime()
))
.toList();

return AdminShuttleBusTimetableResponse.builder()
.nodeInfo(nodeInfos)
.region(table.getRegion())
.routeInfo(routeInfos)
.routeName(table.getRouteName())
.routeType(table.getRouteType())
.subName(table.getSubName())
.build();
}

@JsonNaming(SnakeCaseStrategy.class)
public record NodeInfo(
@Schema(description = "정류소 이름", example = "한기대", requiredMode = REQUIRED)
String name,
@Builder
public record InnerAdminShuttleBusTimetableResponse(
@Schema(description = "운행 지역", example = "CHEONAN_ASAN", requiredMode = REQUIRED)
String region,

@Schema(description = "정류소 이름 추가 설명 (없으면 null)", example = "학화호두과자 앞", requiredMode = NOT_REQUIRED)
String detail
) {
}
@Schema(description = "노선 타입", example = "SHUTTLE", requiredMode = REQUIRED)
String routeType,

@JsonNaming(SnakeCaseStrategy.class)
public record RouteInfo(
@Schema(description = "회차 이름", example = "1회", requiredMode = REQUIRED)
String name,
@Schema(description = "노선 이름", example = "천안 셔틀", requiredMode = REQUIRED)
String routeName,

@Schema(description = "노선 부제목", example = "토요일, 일요일", requiredMode = NOT_REQUIRED)
String subName,

@Schema(description = "회차 세부 이름", example = "(청주역→본교)", requiredMode = NOT_REQUIRED)
String detail,
@Schema(description = "정류소 정보 리스트")
List<InnerNodeInfoResponse> nodeInfo,

@Schema(description = "각 정류소 별 도착 시간 (미정차인 경우 null)", requiredMode = REQUIRED)
List<String> arrivalTime
@Schema(description = "회차별 도착 시간 및 운행 요일 정보 리스트")
List<InnerRouteInfoResponse> routeInfo
) {
public static InnerAdminShuttleBusTimetableResponse from(ShuttleBusTimetable table) {
List<InnerNodeInfoResponse> nodeInfo = table.getNodeInfos()
.stream()
.map(n -> new InnerNodeInfoResponse(
n.getName(),
n.getDetail()
))
.toList();

List<InnerRouteInfoResponse> routeInfo = table.getRouteInfos()
.stream()
.map(r -> new InnerRouteInfoResponse(
r.getName(),
r.getDetail(),
r.getArrivalTime()
))
.toList();

return InnerAdminShuttleBusTimetableResponse.builder()
.nodeInfo(nodeInfo)
.region(table.getRegion())
.routeInfo(routeInfo)
.routeName(table.getRouteName())
.routeType(table.getRouteType())
.subName(table.getSubName())
.build();
}

@JsonNaming(SnakeCaseStrategy.class)
public record InnerNodeInfoResponse(
@Schema(description = "정류소 이름", example = "한기대", requiredMode = REQUIRED)
String name,

@Schema(description = "정류소 이름 추가 설명 (없으면 null)", example = "학화호두과자 앞", requiredMode = NOT_REQUIRED)
String detail
) {
}

@JsonNaming(SnakeCaseStrategy.class)
public record InnerRouteInfoResponse(
@Schema(description = "회차 이름", example = "1회", requiredMode = REQUIRED)
String name,

@Schema(description = "회차 세부 이름", example = "(청주역→본교)", requiredMode = NOT_REQUIRED)
String detail,

@Schema(
description = "각 정류소 별 도착 시간 (미정차인 경우 null)",
example = "[\"08:00\", \"09:00\"]",
requiredMode = REQUIRED
)
List<String> arrivalTime
) {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package in.koreatech.koin.admin.bus.shuttle.extractor;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;

public class PoiCellExtractor {

private static final DataFormatter FORMATTER = new DataFormatter();

public static String extractStringValue(Cell cell) {
String value = FORMATTER.formatCellValue(cell);

return value != null ? value.trim() : "";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package in.koreatech.koin.admin.bus.shuttle.extractor;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;

import in.koreatech.koin.admin.bus.shuttle.model.RouteName;
import in.koreatech.koin.admin.bus.shuttle.model.RouteType;
import in.koreatech.koin.admin.bus.shuttle.model.SubName;
import in.koreatech.koin.domain.bus.enums.ShuttleBusRegion;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class ShuttleBusMetaDataExtractor {

private final Sheet sheet;

private static final int REGION_ROW = 0;
private static final int REGION_COL = 1;

private static final int ROUTE_TYPE_ROW = 1;
private static final int ROUTE_TYPE_COL = 1;

public ShuttleBusRegion extractRegion() {
Row row = sheet.getRow(REGION_ROW);
Cell cell = row.getCell(REGION_COL);

return ShuttleBusRegion.of(PoiCellExtractor.extractStringValue(cell));
}

public RouteType extractRouteType() {
Row row = sheet.getRow(ROUTE_TYPE_ROW);
Cell cell = row.getCell(ROUTE_TYPE_COL);

return RouteType.of(PoiCellExtractor.extractStringValue(cell));
}

public RouteName extractRouteName() {
String sheetName = sheet.getSheetName();

return RouteName.of(sheetName);
}

public SubName extractSubName() {
String sheetName = sheet.getSheetName();

return SubName.of(sheetName);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package in.koreatech.koin.admin.bus.shuttle.util;
package in.koreatech.koin.admin.bus.shuttle.extractor;

import static in.koreatech.koin.admin.bus.shuttle.model.ShuttleBusTimetable.NodeInfo;

Expand All @@ -10,12 +10,17 @@
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.util.StringUtils;

public class ShuttleBusNodeInfoParser {
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class ShuttleBusNodeInfoExtractor {

private final Sheet sheet;

private static final int START_BUS_STOP_ROW = 5;
private static final int START_BUS_STOP_COL = 0;

public static List<NodeInfo> getNodeInfos(Sheet sheet) {
public List<NodeInfo> extractNodeInfos() {
List<NodeInfo> nodeInfos = new ArrayList<>();

for (int i = START_BUS_STOP_ROW; i <= sheet.getLastRowNum(); i++) {
Expand All @@ -26,18 +31,15 @@ public static List<NodeInfo> getNodeInfos(Sheet sheet) {
}

Cell cell = row.getCell(START_BUS_STOP_COL);
String nameWithDetail = ExcelStringUtil.getCellValueToString(cell);
String nameWithDetail = (cell == null) ? "" : PoiCellExtractor.extractStringValue(cell);

if (cell == null || !StringUtils.hasText(nameWithDetail)) {
if (!StringUtils.hasText(nameWithDetail)) {
break;
}

nameWithDetail = nameWithDetail.trim();

String name = ExcelStringUtil.extractNameWithoutBrackets(nameWithDetail);
String detail = ExcelStringUtil.extractDetailFromBrackets(nameWithDetail);

nodeInfos.add(NodeInfo.of(name, detail));
nodeInfos.add(NodeInfo.of(nameWithDetail));
}

return nodeInfos;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package in.koreatech.koin.admin.bus.shuttle.util;
package in.koreatech.koin.admin.bus.shuttle.extractor;

import static in.koreatech.koin.admin.bus.shuttle.model.ShuttleBusTimetable.RouteInfo;
import static in.koreatech.koin.admin.bus.shuttle.model.ShuttleBusTimetable.RouteInfo.InnerNameDetail;
Expand All @@ -15,23 +15,28 @@

import in.koreatech.koin.admin.bus.shuttle.enums.RunningDays;
import in.koreatech.koin.admin.bus.shuttle.model.ArrivalTime;
import in.koreatech.koin.admin.bus.shuttle.util.ExcelRangeUtil;
import lombok.RequiredArgsConstructor;

public class ShuttleBusRouteInfoParser {
@RequiredArgsConstructor
public class ShuttleBusRouteInfoExtractor {

private final Sheet sheet;

private static final int START_HEADER_ROW = 3;
private static final int START_DETAIL_ROW = 4;
private static final int START_TIME_DATA_ROW = 5;

private static final int START_COL = 1;

public static List<RouteInfo> getRouteInfos(Sheet sheet) {
List<InnerNameDetail> innerNameDetails = extractRouteNameDetails(sheet);
public List<RouteInfo> extractRouteInfos() {
List<InnerNameDetail> innerNameDetails = extractRouteNameDetails();

List<RunningDays> runningDays = innerNameDetails.stream()
.map(RunningDays::from)
.toList();

List<ArrivalTime> arrivalTimes = extractArrivalTimes(sheet);
List<ArrivalTime> arrivalTimes = extractArrivalTimes();

return IntStream.range(0, innerNameDetails.size())
.mapToObj(i -> from(
Expand All @@ -42,7 +47,7 @@ public static List<RouteInfo> getRouteInfos(Sheet sheet) {
.toList();
}

private static List<InnerNameDetail> extractRouteNameDetails(Sheet sheet) {
private List<InnerNameDetail> extractRouteNameDetails() {
List<InnerNameDetail> innerNameDetails = new ArrayList<>();

Row headerRow = sheet.getRow(START_HEADER_ROW);
Expand All @@ -59,12 +64,12 @@ private static List<InnerNameDetail> extractRouteNameDetails(Sheet sheet) {
break;
}

String name = nameCell.getStringCellValue().trim();
String name = PoiCellExtractor.extractStringValue(nameCell);

Cell detailCell = detailRow.getCell(col);

String detail = (detailCell != null && StringUtils.hasText(detailCell.toString()))
? detailCell.getStringCellValue().trim()
? PoiCellExtractor.extractStringValue(detailCell)
: null;

innerNameDetails.add(InnerNameDetail.of(name, detail));
Expand All @@ -73,7 +78,7 @@ private static List<InnerNameDetail> extractRouteNameDetails(Sheet sheet) {
return innerNameDetails;
}

private static List<ArrivalTime> extractArrivalTimes(Sheet sheet) {
private List<ArrivalTime> extractArrivalTimes() {
List<ArrivalTime> arrivalTimes = new ArrayList<>();

for (int colNum = START_COL;
Expand All @@ -93,7 +98,7 @@ private static List<ArrivalTime> extractArrivalTimes(Sheet sheet) {
}

Cell cell = row.getCell(colNum);
String strTime = ExcelStringUtil.getCellValueToString(cell);
String strTime = PoiCellExtractor.extractStringValue(cell);

if (cell == null || !StringUtils.hasText(strTime)) {
times.add(null);
Expand Down
Loading
Loading