Skip to content
MinJeong Hong edited this page Jan 9, 2026 · 13 revisions
import { MissionUtils } from '@woowacourse/mission-utils';

export default class InputView {
  async readCommand() {
    return await MissionUtils.Console.readLineAsync('입력');
  }

  async readNickname() {
    return await MissionUtils.Console.readLineAsync('닉네임을 입력하세요.');
  }

  async readTime() {
    return await MissionUtils.Console.readLineAsync('시간을 입력하세요. (HH:MM)');
  }

  // 필요한 메서드들 추가...
}
import { MissionUtils } from '@woowacourse/mission-utils';

export default class OutputView {
  printStartMessage() {
    MissionUtils.Console.print('[프로그램 시작]');
  }

  printEndMessage() {
    MissionUtils.Console.print('[프로그램 종료]');
  }

  printError(message) {
    MissionUtils.Console.print(message);
  }

  printResult(data) {
    MissionUtils.Console.print(data);
  }

  // 필요한 메서드들 추가...
}
export default class Validator {
  validateCommand(input) {
    // 스텁: 나중에 구현
  }

  // 필요한 메서드들 추가...
}
/*
코어북: 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 순서를 고정
*/

Pipeline

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;
  }
}

Pipeline

import InputView from './view/InputView.js';
import OutputView from './view/OutputView.js';
import Validator from './validator/Validator.js';

class App {
  constructor() {
    this.input = new InputView();
    this.output = new OutputView();
    this.validator = new Validator();
  }

  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)
      }
    }
  }

  // ===== 파이프라인 예제들 =====

  // 1. 단순 Number 파싱
  async #readAmountPipeline() {
    // Raw 입력
    const amountRaw = await this.#readUntilValid(
      () => this.input.readAmount(),
      (v) => this.validator.validateAmountRaw(v) // 형식 검증
    );

    // Parse
    const amount = Number(amountRaw);

    // Validate (parsed)
    this.validator.validateAmount(amount); // 범위, 조건 검증

    return amount;
  }

  // 2. split + map 파싱 (배열)
  async #readWinningNumbersPipeline() {
    // Raw 입력
    const numbersRaw = await this.#readUntilValid(
      () => this.input.readWinningNumbers(),
      (v) => this.validator.validateWinningNumbersRaw(v) // 형식 검증
    );

    // Parse
    const numbers = numbersRaw
      .split(',')
      .map((n) => n.trim())
      .map((n) => Number(n));

    // Validate (parsed)
    this.validator.validateWinningNumbers(numbers); // 범위, 중복 등

    return numbers;
  }

  // 3. split + map 파싱 (시간)
  async #readTimePipeline() {
    // Raw 입력
    const timeRaw = await this.#readUntilValid(
      () => this.input.readTime(),
      (v) => this.validator.validateTimeRaw(v) // "HH:MM" 형식만
    );

    // Parse
    const [hourStr, minStr] = timeRaw.split(':').map((s) => s.trim());
    const hour = Number(hourStr);
    const min = Number(minStr);

    // Validate (parsed)
    this.validator.validateTime(hour, min); // 범위 검증

    return [hour, min];
  }

  // 4. 간단한 문자열 (파싱 불필요)
  async #readCommandPipeline() {
    return await this.#readUntilValid(
      () => this.input.readCommand(),
      (v) => this.validator.validateCommand(v)
    );
  }

  // 5. 다른 값과의 관계 검증이 필요한 경우
  async #readBonusNumberPipeline(winningNumbers) {
    // Raw 입력
    const bonusRaw = await this.#readUntilValid(
      () => this.input.readBonusNumber(),
      (v) => this.validator.validateBonusNumberRaw(v) // 형식 검증
    );

    // Parse
    const bonusNumber = Number(bonusRaw);

    // Validate (parsed) - 다른 값과의 관계 검증
    this.validator.validateBonusNumber(winningNumbers, bonusNumber);

    return bonusNumber;
  }

  // 6. 복잡한 파싱 (2차원 배열 등)
  async #readPurchaseQuantityPipeline() {
    // Raw 입력
    const purchaseRaw = await this.#readUntilValid(
      () => this.input.readPurchaseQuantity(),
      (v) => this.validator.validatePurchaseForm(v) // 형식 검증
    );

    // Parse (복잡한 파싱)
    const purchaseQuantity = purchaseRaw
      .split('],[')
      .map((item) => item.replace(/[\[\]]/g, ''))
      .map((item) => item.split('-'))
      .map(([name, quantity]) => [name, Number(quantity)]);

    // Validate (parsed)
    this.validator.validatePurchaseQuantity(purchaseQuantity);

    return purchaseQuantity; // [['콜라', 3], ['사이다', 5]]
  }

  // 7. 날짜 파싱
  async #readDatePipeline() {
    // Raw 입력
    const dateRaw = await this.#readUntilValid(
      () => this.input.readDate(),
      (v) => this.validator.validateDateRaw(v) // 형식 검증
    );

    // Parse
    const date = Number(dateRaw);

    // Validate (parsed)
    this.validator.validateDate(date); // 범위 검증 (1~31일 등)

    return date;
  }

  // 8. 배열 파싱 (문자열 배열)
  async #readNamesPipeline() {
    // Raw 입력
    const namesRaw = await this.#readUntilValid(
      () => this.input.readNames(),
      (v) => this.validator.validateNamesRaw(v) // 형식 검증
    );

    // Parse
    const names = namesRaw
      .split(',')
      .map((n) => n.trim());

    // Validate (parsed)
    this.validator.validateNames(names); // 중복, 개수 등

    return names;
  }
}

export default App;

Clone this wiki locally