-
Notifications
You must be signed in to change notification settings - Fork 0
Home
MinJeong Hong edited this page Jan 9, 2026
·
13 revisions
import { MissionUtils } from '@woowacourse/mission-utils';
export default class InputView {
async readCommand() {
return await Console.readLineAsync('입력');
}
async readNickname() {
return await MissionUtils.Console.readLineAsync('닉네임을 입력하세요.');
}
async readTime() {
return await Console.readLineAsync('시간을 입력하세요. (HH:MM)');
}
// 필요한 메서드들 추가...
}import { MissionUtils } from '@woowacourse/mission-utils';
export default class OutputView {
printStartMessage() {
Console.print('[프로그램 시작]');
}
printEndMessage() {
Console.print('[프로그램 종료]');
}
printError(message) {
Console.print(message);
}
printResult(data) {
Console.print(data);
}
// 필요한 메서드들 추가...
}export default class Validator{
}/*
코어북: App.js 패턴 템플릿 (FULL SHEET)
- 목표: 입력/검증/분기/반복/세트입력/복귀를 App.js에서 “같은 방식”으로 조립
- 규칙: 입력 유효성 실패 시 에러 출력 후 재입력은 #readUntilValid가 책임짐
- 전제: Validator는 검증 실패 시 throw Error(...) 한다
*/
/*
목차 (누락 체크)
- #readUntilValid 재입력 코어 ✅
- 메뉴 루프(run) 구조 ✅
- 메뉴 복귀(모드 중단) 패턴 ✅
- (1) return 기반: 모드 내부에서 catch 후 return ✅
- (2) throw 기반: 입력/모드 실패를 throw로 올리고 run에서 catch ✅ ← 추가됨(그거)
- Y/N 반복 패턴 ✅
- 세트 입력(트랜잭션) 패턴 ✅
- 입력 파이프라인(Raw→Parse→Validate→Use) ✅
- 전체 재입력 패턴(= 세트 입력 트랜잭션 예시) ✅
*/
import InputView from './view/InputView.js';
import OutputView from './view/OutputView.js';
import Validator from './validator/Validator.js';
export default class App {
#running = false;
constructor() {
// - App 생명주기 동안 공유 (거의 모든 미션에서 고정)
this.input = new InputView();
this.output = new OutputView();
this.validator = new Validator();
}
// =========================
// 메뉴 루프(run) 구조
// - while: 프로그램 생명주기
// - command 입력 → 분기 실행 → 다시 메뉴
// - 메뉴 복귀(throw 기반)까지 포함하려면 run에서 try/catch로 감싼다 ✅
// =========================
async run() {
this.#running = true;
// =========================
// 메뉴 분기 2가지 중 택1
// (A) switch 분기
// (B) handlers 분기 (권장: 커맨드 늘어날수록 유리)
// =========================
// (B) handlers 분기: command -> 실행함수
const handlers = {
'1': async () => this.#mode1(), // 세트 입력(트랜잭션) 예시
'2': async () => this.#mode2(), // Y/N 반복 예시
'3': async () => this.#mode3(), // 메뉴 복귀(모드 중단) 예시 (return 기반)
'4': async () => this.#mode4(), // 메뉴 복귀(throw 기반) 예시 (그거)
q: async () => this.#exit(),
};
while (this.#running) {
try {
// - 메뉴 출력
this.output.printMenu?.();
// - 메뉴 입력 + 검증
const command = await this.#readUntilValid(
() => this.input.readCommand(),
[
(v) => this.validator.validateCommand(v, Object.keys(handlers)),
]
);
// =========================
// (B) handlers 실행
// =========================
await handlers[command]();
// =========================
// (A) switch 실행 (원하면 위 handlers 대신 아래 사용)
// =========================
/*
switch (command) {
case '1':
await this.#mode1();
break;
case '2':
await this.#mode2();
break;
case '3':
await this.#mode3();
break;
case '4':
await this.#mode4();
break;
case 'q':
await this.#exit();
break;
default:
// - validateCommand에서 막으면 사실상 필요 없음
this.output.printError('[ERROR] 잘못된 형식을 입력하였습니다.');
}
*/
} catch (e) {
// =========================
// 메뉴 복귀(throw 기반) 포인트 ✅
// - 모드/입력에서 throw로 "중단 신호"가 올라오면 여기서 잡고 메뉴로 복귀
// - 여기서 에러를 다시 출력할지 여부는 정책:
// - 보통은 하위에서 이미 printError 했으면 여기서는 아무 것도 안 함
// =========================
continue;
}
}
}
// =====================================================
// #readUntilValid 재입력 코어 (메인)
// - readFn: 입력을 읽는 함수 (async)
// - validators: 검증 함수 배열 (value -> void, 실패 시 throw)
// - 실패 시: 에러 출력 후 같은 입력 다시 받기
// =====================================================
async #readUntilValid(readFn, validators = []) {
while (true) {
try {
const value = await readFn();
validators.forEach((v) => v(value));
return value;
} catch (e) {
this.output.printError(e.message);
}
}
}
// =====================================================
// 메뉴 복귀(모드 중단) 패턴 - (throw 기반) ✅ (그거)
// - 정책: “이 입력은 틀리면 같은 질문 재입력 X, 모드 중단 후 메뉴로 복귀”
// - 구현: 한 번만 읽고 검증 실패 시 출력 후 throw
// - 사용: mode 내부에서 await #readOnceOrThrow(...) 호출
// - run()이 try/catch로 감싸고 있으므로 throw가 올라오면 메뉴로 복귀한다
// =====================================================
async #readOnceOrThrow(readFn, validators = []) {
try {
const value = await readFn();
validators.forEach((v) => v(value));
return value;
} catch (e) {
this.output.printError(e.message);
throw e; // ✅ 메뉴 복귀 신호
}
}
// =====================================================
// 메뉴 복귀(모드 중단) 패턴 - (return 기반)
// - 정책: “모드 안에서 실패하면 모드만 중단하고 메뉴로 돌아감”
// - 구현: mode 내부를 try/catch로 감싸고 return으로 빠져나오기
// - 주의: 이 패턴은 throw로 상위 전파 없이도 구현 가능
// =====================================================
async #withModeAbort(modeFn) {
try {
await modeFn();
} catch (e) {
// - modeFn 내부에서 throw한 경우(Validator/Service 등)
this.output.printError(e.message);
return; // ✅ 모드 중단 후 메뉴로 복귀 (run의 while로 돌아감)
}
}
// =====================================================
// Y/N 반복 패턴
// - actionFn: 한 번 실행할 작업
// - questionFn: Y/N 입력 함수
// - 정책: Y면 반복, N이면 종료(return)
// - 주의: 여기서 return은 #runWithContinue만 종료 → 호출한 mode로 복귀 → run의 메뉴루프 계속
// =====================================================
async #runWithContinue(actionFn, questionFn) {
while (true) {
await actionFn();
const answer = await this.#readUntilValid(
() => questionFn(),
[
// - 정규화는 아래 if에서 하든, Validator에서 처리하든 한 곳으로 고정
(v) => this.validator.validateYesOrNo(v),
]
);
if (answer.trim().toUpperCase() === 'N') return;
}
}
// =====================================================
// 세트 입력(트랜잭션) 패턴 (= 전체 재입력 패턴)
// - 정책: “휴일 입력이 틀리면 평일부터 다시 입력”
// - 구현: readFn에서 A,B를 한 번에 읽고 객체로 반환
// validators에서 A검증/B검증/관계검증 수행
// 실패하면 #readUntilValid가 세트 전체를 다시 받음
// =====================================================
async #readWeekdayAndHoliday() {
return await this.#readUntilValid(
async () => {
const weekdayWorkersRaw = await this.input.readWeekdayWorker();
const holidayWorkersRaw = await this.input.readHolidayWorker();
return { weekdayWorkersRaw, holidayWorkersRaw };
},
[
({ weekdayWorkersRaw, holidayWorkersRaw }) => {
this.validator.validateWeekdayWorkers(weekdayWorkersRaw);
this.validator.validateHolidayWorkers(holidayWorkersRaw);
this.validator.validateWorkerPair(weekdayWorkersRaw, holidayWorkersRaw);
},
]
);
}
// =====================================================
// 입력 파이프라인(Raw→Parse→Validate→Use)
// - Raw: 문자열 입력
// - Parse: 숫자/배열/객체 변환
// - Validate: 변환된 값 기준 도메인 검증
// - Use: 서비스/도메인 실행 후 출력
// =====================================================
async #readAmountPipelineExample() {
// Raw
const amountRaw = await this.#readUntilValid(
() => this.input.readAmount(),
[
// - 문자열 형식/공백/숫자 여부 등 (Raw 검증)
(v) => this.validator.validateAmountRaw(v),
]
);
// Parse
const amount = Number(amountRaw);
// Validate (parsed)
this.validator.validateAmount(amount);
// Use (예시)
this.output.printAmount?.(amount);
return amount;
}
// =========================
// 모드 예시들 (App.js에서 “조립”)
// =========================
// -----------------------------------------------------
// mode1: 세트 입력(트랜잭션) + 파이프라인(간단 파싱) 예시
// -----------------------------------------------------
async #mode1() {
// - “휴일이 틀리면 평일부터 재입력” 요구사항을 그대로 만족
const { weekdayWorkersRaw, holidayWorkersRaw } = await this.#readWeekdayAndHoliday();
// Parse
const weekdayWorkers = weekdayWorkersRaw.split(',').map((v) => v.trim());
const holidayWorkers = holidayWorkersRaw.split(',').map((v) => v.trim());
// Validate(Parsed)가 필요하면 여기서 추가 검증
// this.validator.validateWorkersParsed(weekdayWorkers, holidayWorkers);
// Use
this.output.printWorkers?.(weekdayWorkers, holidayWorkers);
}
// -----------------------------------------------------
// mode2: Y/N 반복 예시
// -----------------------------------------------------
async #mode2() {
await this.#runWithContinue(
async () => {
await this.#readAmountPipelineExample();
// - 반복할 작업을 여기에
},
() => this.input.readYesOrNo()
);
}
// -----------------------------------------------------
// mode3: 메뉴 복귀(모드 중단) - return 기반 예시
// - 특징: 이 모드 안에서 에러가 나면 "모드만" 종료하고 메뉴로 돌아감
// -----------------------------------------------------
async #mode3() {
await this.#withModeAbort(async () => {
const name = await this.#readUntilValid(
() => this.input.readName(),
[(v) => this.validator.validateName(v)]
);
const time = await this.#readUntilValid(
() => this.input.readTime(),
[(v) => this.validator.validateTime(v)]
);
this.output.printOk?.(name, time);
});
}
// -----------------------------------------------------
// mode4: 메뉴 복귀(모드 중단) - throw 기반 예시 ✅ (그거)
// - 특징: 특정 입력은 "재입력"이 아니라 "실패 시 즉시 메뉴로 복귀" 정책
// - 구현: #readOnceOrThrow가 throw → run() catch → 메뉴 복귀
// -----------------------------------------------------
async #mode4() {
const crewArr = this.manager?.getCrew?.() ?? []; // 예시: 있으면 사용
const nickName = await this.#readOnceOrThrow(
() => this.input.readNickname(),
[
(v) => this.validator.validateRegisteredCrew?.(v, crewArr),
]
);
const timeRaw = await this.#readOnceOrThrow(
() => this.input.readCheckIn(),
[(v) => this.validator.validateTime(v)]
);
// Parse
const [hour, min] = timeRaw.split(':').map((x) => x.trim());
// Use
this.output.printCheckIn?.({ nickName, hour, min });
}
async #exit() {
this.output.printEndMessage?.();
this.#running = false;
}
}
/*
코어 문장(정리)
- 재입력: #readUntilValid가 담당 (같은 질문 다시)
- 메뉴 루프: run의 while이 담당 (메뉴 화면 다시)
- 메뉴 복귀(모드 중단):
- return 기반: #withModeAbort 내부에서 catch 후 return → 모드만 종료
- throw 기반: #readOnceOrThrow가 throw → run의 catch가 잡고 continue → 메뉴로 복귀
- Y/N 반복: #runWithContinue로 action 반복 제어 (N이면 해당 함수만 종료)
- 세트 입력(트랜잭션): readFn에서 A+B 묶고 validators에서 세트 검증 → 실패 시 세트 전체 재입력
- 파이프라인: Raw→Parse→Validate→Use 순서를 고정
*/class App {
constructor() {
this.input = new InputView();
this.validator = new Validator();
this.output = new OutputView();
}
async run() {
// 파이프라인 사용
const amount = await this.#readAmountPipeline();
const winningNumbers = await this.#readWinningNumbersPipeline();
const bonusNumber = await this.#readBonusNumberPipeline(winningNumbers);
// 비즈니스 로직
// ...
}
// ===== 공통 유틸리티 (재사용) =====
async #readUntilValid(readFn, validateFn) {
while (true) {
try {
const value = await readFn();
validateFn(value);
return value;
} catch (error) {
this.output.printError(error.message);
// 재입력 (continue)
}
}
}
// ===== 각 입력별 파이프라인 (private) =====
async #readAmountPipeline() {
// 1. Raw 입력
const amountRaw = await this.#readUntilValid(
() => this.input.readAmount(),
(v) => this.validator.validateAmountRaw(v)
);
// 2. Parse
const amount = Number(amountRaw);
// 3. Validate (parsed)
this.validator.validateAmount(amount);
// 4. Return
return amount;
}
async #readWinningNumbersPipeline() {
const numbersRaw = await this.#readUntilValid(
() => this.input.readWinningNumbers(),
(v) => this.validator.validateWinningNumbersRaw(v)
);
const numbers = numbersRaw
.split(',')
.map((n) => Number(n.trim()));
this.validator.validateWinningNumbers(numbers);
return numbers;
}
async #readBonusNumberPipeline(winningNumbers) {
const bonusRaw = await this.#readUntilValid(
() => this.input.readBonusNumber(),
(v) => this.validator.validateBonusNumberRaw(v)
);
const bonusNumber = Number(bonusRaw);
// 다른 값과의 관계 검증도 여기서!
this.validator.validateBonusNumber(winningNumbers, bonusNumber);
return bonusNumber;
}
}