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', + }, + ], +};