diff --git a/.vscode/setting.json b/.vscode/setting.json
new file mode 100644
index 0000000..04eaf11
--- /dev/null
+++ b/.vscode/setting.json
@@ -0,0 +1,6 @@
+{
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.tabSize": 2,
+ "git.pruneOnFetch": true
+}
diff --git a/package-lock.json b/package-lock.json
index 3b9be44..45a5a2c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"ink-text-input": "^5.0.1",
"meow": "^11.0.0",
"mobx": "^6.9.0",
- "prettier": "^2.8.8",
+ "prettier": "3.0.1",
"react": "^18.2.0"
},
"bin": {
@@ -4854,14 +4854,14 @@
}
},
"node_modules/prettier": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
- "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
+ "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==",
"bin": {
- "prettier": "bin-prettier.js"
+ "prettier": "bin/prettier.cjs"
},
"engines": {
- "node": ">=10.13.0"
+ "node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
@@ -9522,9 +9522,9 @@
"peer": true
},
"prettier": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
- "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
+ "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ=="
},
"pretty-ms": {
"version": "8.0.0",
diff --git a/package.json b/package.json
index 2891f51..3991016 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,8 @@
"build": "babel --out-dir=dist source",
"dev": "babel --out-dir=dist --watch source",
"test": "prettier --check . ",
- "start": "node dist/cli.js"
+ "start": "node dist/cli.js",
+ "format": "prettier --write ."
},
"files": [
"dist"
@@ -32,7 +33,7 @@
"meow": "^11.0.0",
"mobx": "^6.9.0",
"react": "^18.2.0",
- "prettier": "^2.8.8"
+ "prettier": "3.0.1"
},
"devDependencies": {
"@babel/cli": "^7.21.0",
diff --git a/source/api/kakao.js b/source/api/kakao.js
new file mode 100644
index 0000000..fc5394f
--- /dev/null
+++ b/source/api/kakao.js
@@ -0,0 +1,32 @@
+import axios from 'axios';
+import dotenv from 'dotenv';
+
+export const fetchKakaoShops = (query, category) => {
+ dotenv.config();
+
+ const API_KEY = process.env.KAKAO_API_KEY;
+
+ const url = `https://dapi.kakao.com/v2/local/search/keyword.json?query=${query}&size=3`;
+
+ if (category == 'cafe') {
+ return axios.get(url + '&category_group_code=CE7', {
+ headers: {
+ Authorization: `KakaoAK ${API_KEY}`,
+ },
+ });
+ }
+
+ if (category == 'restaurant') {
+ return axios.get(url + '&category_group_code=FD6', {
+ headers: {
+ Authorization: `KakaoAK ${API_KEY}`,
+ },
+ });
+ }
+
+ return axios.get(url, {
+ headers: {
+ Authorization: `KakaoAK ${API_KEY}`,
+ },
+ });
+};
diff --git a/source/component/shop_detail.js b/source/component/shop_detail.js
new file mode 100644
index 0000000..9650a30
--- /dev/null
+++ b/source/component/shop_detail.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import {Text, Newline, Box} from 'ink';
+
+/**
+ *
+ * @param {Object} data
+ * @description
+ * data = {
+ * id: '1',
+ * title: 'The 5th Wave',
+ * location: '서울시 동작구 상도로 369',
+ * nearStation: '상도역',
+ * openTime: '09:00',
+ * closeTime: '22:00',
+ * menu: [
+ * {
+ * name: '카페라떼',
+ * price: 4000,
+ * },
+ * ],
+ * starRate: 4.5,
+ * reviews: [],
+ * }
+ */
+const ShopDetail = ({data}) => {
+ const starRateRounded = Math.round(data.starRate);
+
+ const starRateString = '⭐'.repeat(starRateRounded);
+
+ return (
+ <>
+ {'{'}
+
+
+
+ "id" : "{data.id}",
+
+
+
+ "title" : "{data.title}",
+
+
+
+ "location" : "{data.location}",
+
+
+
+ "nearStation" : "{data.nearStation}",
+
+
+
+ "openTime" : "{data.openTime}" - "{data.closeTime}",
+
+
+
+ "menu" : {'['}
+
+ {data.menu.map((item, index, array) => (
+
+ {' '}
+ {'{'} "{item.name}" : {item.price} {'}'}
+ {index !== array.length - 1 ? ',' : ''}
+
+
+ ))}
+ {']'},
+
+
+
+ "starRate" : "{starRateString}({data.starRate})",
+
+
+
+ "reviews" : {'['}
+
+ {data.reviews.map((item, index, array) => (
+
+ {' '}
+ {'{'} "{item.writer}" : "{item.content} ({item.starRate})" {'}'}
+ {index !== array.length - 1 ? ',' : ''}
+
+
+ ))}
+ {']'}
+
+
+
+ {'}'}
+ >
+ );
+};
+export default ShopDetail;
diff --git a/source/component/shop_post.js b/source/component/shop_post.js
new file mode 100644
index 0000000..21a393a
--- /dev/null
+++ b/source/component/shop_post.js
@@ -0,0 +1,503 @@
+import React, {useState} from 'react';
+import {Text, Box, useInput, Newline, Spacer} from 'ink';
+import TextInput from 'ink-text-input';
+import {fetchKakaoShops} from '../api/kakao.js';
+
+const dayContainer = (isFocused, isSelected, day, key) => {
+ var color = 'white';
+
+ if (isSelected) {
+ color = 'green';
+ }
+ if (isFocused) {
+ color = 'yellow';
+ }
+
+ return (
+
+ {day}
+
+ );
+};
+
+const editContainer = (isFocused, text, key) => {
+ var color = 'white';
+
+ if (isFocused) {
+ color = 'yellow';
+ }
+
+ return (
+
+ {text}
+
+ );
+};
+/**
+ * @param {string} category
+ * @returns {void}
+ * @description
+ * 카테고리에 따라 카카오 API를 호출하여 가게 리스트를 가져온다.
+ * 카테고리는 cafe, restaurant로 구분된다.
+ * 이외의 카테고리나 넣지 않으면 모든 카테고리에서 검색한다.
+ */
+const ShopPost = ({category}) => {
+ const [lastKeyPress, setLastKeyPress] = useState(null);
+
+ const [inputStep, setInputStep] = useState(0); // 0: title, 1: openTime, 2: closeTime
+ const [shopTitle, setShopTitle] = useState('');
+ const [kakaoShops, setKakaoShops] = useState([]);
+ const [selectedShopIndex, setSelectedShopIndex] = useState(0);
+
+ const [openDayList, setOpenDayList] = useState([]); // 추가: 오픈 요일
+ const [openDayIndex, setOpenDayIndex] = useState(0); // 추가: 오픈 요일 인덱스
+ const dayList = ['월', '화', '수', '목', '금', '토', '일', '']; // 요일 리스트
+
+ const [openTimeHour, setOpenTimeHour] = useState(0);
+ const [openTimeMinute, setOpenTimeMinute] = useState(0);
+
+ const [closeTimeHour, setCloseTimeHour] = useState(0);
+ const [closeTimeMinute, setCloseTimeMinute] = useState(0);
+
+ const [focus, setFocus] = useState(0); // 0 for hour, 1 for minute
+
+ // menu
+ const [menuList, setMenuList] = useState([]);
+ const [menuName, setMenuName] = useState('');
+ const [menuPrice, setMenuPrice] = useState(0);
+
+ const [confirmCommand, setConfirmCommand] = useState(''); // 입력 확인
+ const [isEdit, setIsEdit] = useState(false); // 입력 수정
+ const editList = ['title', 'openDay', 'openTime', 'closeTime', 'menu', '']; // 수정 가능한 목록
+
+ useInput((input, key) => {
+ if (!key) return;
+ setLastKeyPress(key.name);
+
+ if (inputStep == 0) {
+ if (key.upArrow && selectedShopIndex > 0) {
+ setSelectedShopIndex(selectedShopIndex => selectedShopIndex - 1);
+ }
+ if (key.downArrow && selectedShopIndex < kakaoShops.length - 1) {
+ setSelectedShopIndex(selectedShopIndex => selectedShopIndex + 1);
+ }
+ }
+
+ // 운영 요일 추가
+ if (inputStep == 1) {
+ if (key.tab) {
+ if (openDayIndex == dayList.length - 1) {
+ setOpenDayIndex(0);
+ } else {
+ setOpenDayIndex(openDayIndex => openDayIndex + 1);
+ }
+ }
+
+ if (key.rightArrow) {
+ if (openDayIndex == dayList.length - 1) {
+ setOpenDayIndex(0);
+ } else {
+ setOpenDayIndex(openDayIndex => openDayIndex + 1);
+ }
+ }
+
+ if (key.leftArrow) {
+ if (openDayIndex == 0) {
+ setOpenDayIndex(dayList.length - 1);
+ } else {
+ setOpenDayIndex(openDayIndex => openDayIndex - 1);
+ }
+ }
+
+ if (key.return) {
+ // 완료 버튼 누르면 다음 단계로
+ if (openDayIndex == dayList.length - 1) {
+ setInputStep(prevInputStep => prevInputStep + 1);
+ return;
+ }
+
+ // 오픈 요일 추가, 이미 추가된 요일이면 삭제
+ if (openDayList.includes(dayList[openDayIndex])) {
+ setOpenDayList(
+ openDayList.filter(day => day != dayList[openDayIndex]),
+ );
+ } else {
+ setOpenDayList([...openDayList, dayList[openDayIndex]]);
+ }
+ }
+
+ return;
+ }
+
+ // 시작 시간
+ if (inputStep == 2) {
+ if (key.tab || key.rightArrow) {
+ setFocus((focus + 1) % 2);
+ }
+ if (key.leftArrow) {
+ setFocus((focus - 1 + 2) % 2);
+ }
+ if (key.upArrow) {
+ if (focus == 0) {
+ // hour
+ setOpenTimeHour((openTimeHour + 1) % 24);
+ } else {
+ // minute
+ setOpenTimeMinute((openTimeMinute + 15) % 60);
+ }
+ }
+ if (key.downArrow) {
+ if (focus == 0) {
+ // hour
+ setOpenTimeHour((openTimeHour - 1 + 24) % 24);
+ } else {
+ // minute
+ setOpenTimeMinute((openTimeMinute - 15 + 60) % 60);
+ }
+ }
+ }
+ // 종료 시간
+ if (inputStep == 3) {
+ if (key.tab || key.rightArrow) {
+ setFocus((focus + 1) % 2);
+ }
+ if (key.leftArrow) {
+ setFocus((focus - 1 + 2) % 2);
+ }
+ if (key.upArrow) {
+ if (focus == 0) {
+ // hour
+ setCloseTimeHour((closeTimeHour + 1) % 24);
+ } else {
+ // minute
+ setCloseTimeMinute((closeTimeMinute + 15) % 60);
+ }
+ }
+ if (key.downArrow) {
+ if (focus == 0) {
+ // hour
+ setCloseTimeHour((closeTimeHour - 1 + 24) % 24);
+ } else {
+ // minute
+ setCloseTimeMinute((closeTimeMinute - 15 + 60) % 60);
+ }
+ }
+ }
+
+ if (inputStep == 4) {
+ if (key.tab || key.rightArrow) {
+ setFocus((focus + 1) % 2);
+ }
+ if (key.leftArrow) {
+ setFocus((focus - 1 + 2) % 2);
+ }
+
+ if (key.return) {
+ if (
+ lastKeyPress === 'return' ||
+ menuName.length == 0 ||
+ menuPrice == 0
+ ) {
+ setInputStep(prevInputStep => prevInputStep + 1);
+ setLastKeyPress(null); // reset the last key press
+ return;
+ } else {
+ addMenu();
+ return;
+ }
+ }
+ }
+
+ // step 5는 입력 확인
+
+ // 수정 할 때, 숫자로 입력 수정 위치 변경
+ if (inputStep >= 6) {
+ if (isEdit) {
+ if (key.tab || key.rightArrow) {
+ setFocus((focus + 1) % 6);
+ }
+ if (key.leftArrow) {
+ setFocus((focus - 1 + 6) % 6);
+ }
+
+ if (key.return) {
+ if (focus == 5) {
+ setInputStep(5);
+ } else {
+ setInputStep(focus - 1);
+ }
+
+ setIsEdit(false);
+ setFocus(0);
+ }
+ }
+ }
+
+ // 카카오 가게 데이터가 받아오기 전에 엔터 누르면 오류 발생
+ try {
+ if (key.return) {
+ if (
+ inputStep == 0 &&
+ kakaoShops[selectedShopIndex].place_name == undefined
+ ) {
+ return;
+ }
+
+ setInputStep(prevInputStep => prevInputStep + 1);
+ setFocus(0);
+
+ if (inputStep == 4) {
+ console.log('complete');
+ }
+ }
+ } catch (error) {
+ return;
+ }
+ });
+
+ // 메뉴 추가
+ const addMenu = () => {
+ setMenuList([...menuList, {name: menuName, price: menuPrice}]);
+ setMenuName(''); // 입력 필드 초기화
+ setMenuPrice(0); // 입력 필드 초기화
+ setFocus(0); // 포커스 초기화
+ };
+
+ // 메뉴 삭제
+ const deleteMenu = index => {
+ const newMenuList = [...menuList];
+ newMenuList.splice(index, 1);
+ setMenuList(newMenuList);
+ };
+
+ const searchKakaoShops = async () => {
+ if (!shopTitle.length) {
+ setKakaoShops([]);
+ return;
+ }
+ const response = await fetchKakaoShops(shopTitle, category);
+ setKakaoShops(response.data['documents']);
+ setSelectedShopIndex(0);
+ };
+
+ React.useEffect(() => {
+ searchKakaoShops();
+ }, [shopTitle]);
+
+ return (
+ <>
+ {'{'}
+
+
+ "title" :
+ {inputStep == 0 ? (
+
+ ) : (
+ "{shopTitle}"
+ )}
+
+
+ {kakaoShops.map((shop, index) => (
+
+ {index + 1}. {shop.place_name} ({shop.address_name})
+
+
+ ))}
+
+
+ {inputStep > 0 && (
+
+
+ "location" : "{kakaoShops[selectedShopIndex].address_name}"
+
+
+ "openDay" :
+ {inputStep == 1 ? (
+
+ {dayList.map((day, index) =>
+ // isFocused, isSelected, day
+ dayContainer(
+ index === openDayIndex,
+ openDayList.includes(dayList[index]),
+ day,
+ index,
+ ),
+ )}
+
+ {' 완료'}
+
+
+ ) : (
+ "{openDayList.join(', ')}"
+ )}
+
+
+ )}
+
+ {/* 오픈 시간 */}
+ {inputStep > 1 && (
+
+ "openTime" :
+ {inputStep == 2 ? (
+
+ "
+
+ {openTimeHour.toString().padStart(2, '0')}
+
+ :
+
+ {openTimeMinute.toString().padStart(2, '0')}
+
+ "
+
+ ) : (
+
+ "{openTimeHour.toString().padStart(2, '0')}:
+ {openTimeMinute.toString().padStart(2, '0')}"
+
+ )}
+
+ )}
+
+ {/* 마감 시간 */}
+ {inputStep > 2 && (
+
+ "closeTime" :
+ {inputStep == 3 ? (
+
+ "
+
+ {closeTimeHour.toString().padStart(2, '0')}
+
+ :
+
+ {closeTimeMinute.toString().padStart(2, '0')}
+
+ "
+
+ ) : (
+
+ "{closeTimeHour.toString().padStart(2, '0')}:
+ {closeTimeMinute.toString().padStart(2, '0')}"
+
+ )}
+
+ )}
+
+ {/* 메뉴 */}
+ {inputStep > 3 && (
+
+ "menu" : {'['}
+ {inputStep == 4 ? (
+
+ {menuList.map((menu, index) => (
+
+
+ {'{'}"menuName" : "{menu.name}", "menuPrice" :{' '}
+ {menu.price}
+ {index == menuList.length ? '}' : '},'}
+
+
+ ))}
+
+ {'{'}"menuName" : "
+
+ ", "menuPrice" :
+ setMenuPrice(parseInt(value) || 0)}
+ focus={focus == 1}
+ />
+ {'}'}
+
+
+ ) : (
+
+ {menuList.map((menu, index) => (
+
+
+ {'{'}"menuName" : "{menu.name}", "menuPrice" :{' '}
+ {menu.price}
+ {index == menuList.length - 1 && menuList.length > 1
+ ? '}'
+ : '},'}
+
+
+ ))}
+
+ )}
+ {']'}
+
+ )}
+
+ {'}'}
+
+ {/* 입력 확인 */}
+ {inputStep > 4 && (
+
+
+
+
+ Commands
+
+
+ :wq - save and quit
+
+ :q! - force quit
+
+ :e - edit input
+
+
+ {!isEdit ? (
+ {
+ if (confirmCommand == ':wq') {
+ //TODO : 저장하고 종료
+ process.exit(0);
+ } else if (confirmCommand == ':q!') {
+ //TODO : 저장하지 않고 종료
+ setConfirmCommand('');
+ } else if (confirmCommand == ':e') {
+ //TODO : 입력 수정
+ setIsEdit(true);
+ console.log(inputStep);
+ } else {
+ setConfirmCommand('');
+ }
+ }}
+ />
+ ) : (
+ {confirmCommand}
+ )}
+
+ {isEdit && (
+
+ {editList.map((edit, index) =>
+ editContainer(index === focus, edit, index),
+ )}
+
+ {'취소'}
+
+
+ )}
+
+ )}
+ >
+ );
+};
+
+export default ShopPost;
diff --git a/source/examples/shop.js b/source/examples/shop.js
new file mode 100644
index 0000000..fa2f639
--- /dev/null
+++ b/source/examples/shop.js
@@ -0,0 +1,31 @@
+export default {
+ id: 1423,
+ title: 'The 5th Wave',
+ location: '서울시 동작구 상도로 369',
+ nearStation: '상도역',
+ openTime: '09:00',
+ closeTime: '22:00',
+ menu: [
+ {
+ name: '카페라떼',
+ price: 4000,
+ },
+ ],
+ starRate: 4.5,
+ reviews: [
+ {
+ id: 1,
+ writer: '김철수',
+ content: '매장이 깔끔하고 좋아요',
+ starRate: 4,
+ date: '2019-01-01',
+ },
+ {
+ id: 2,
+ writer: '김영희',
+ content: '불결해요',
+ starRate: 2,
+ date: '2019-01-02',
+ },
+ ],
+};