199199 'id' : 'python-basics' ,
200200 'name' : 'Python 기초' ,
201201 'description' : '언어 기본기와 표준 라이브러리로 로컬 Python 감각을 만든다.' ,
202+ 'folder' : 'basics' ,
202203 'categories' : ['30days' , 'advancedPython' , 'builtins' ],
203204 },
204205 {
205206 'id' : 'data-analysis' ,
206207 'name' : '데이터 분석' ,
207208 'description' : '표 데이터, SQL, 데이터 계약을 다루는 분석 경로다.' ,
209+ 'folder' : 'dataAnalysis' ,
208210 'categories' : ['pandas' , 'numpy' , 'polars' , 'duckdb' , 'pydantic' ],
209211 },
210212 {
211213 'id' : 'visualization' ,
212214 'name' : '시각화' ,
213215 'description' : '정적·통계·인터랙티브·지도 시각화를 구분한다.' ,
216+ 'folder' : 'visualization' ,
214217 'categories' : ['matplotlib' , 'seaborn' , 'plotly' , 'altair' , 'folium' ],
215218 },
216219 {
217220 'id' : 'math-stat-ml' ,
218221 'name' : '수학·통계·ML' ,
219222 'description' : '수학 계산, 통계 모델, 머신러닝과 그래프 분석 경로다.' ,
223+ 'folder' : 'mathStatsMl' ,
220224 'categories' : ['sympy' , 'scipy' , 'statsmodels' , 'sklearn' , 'networkx' ],
221225 },
222226 {
223227 'id' : 'automation' ,
224228 'name' : '자동화' ,
225229 'description' : '반복 작업을 로컬 실행, 브라우저, 업무 도구 단위로 나눈다.' ,
230+ 'folder' : 'automation' ,
226231 'children' : [
227232 {
228233 'id' : 'browser-automation' ,
229234 'name' : '브라우저 자동화' ,
230235 'description' : '화면 점검, 폼 입력, 증거 저장, E2E 흐름을 다룬다.' ,
236+ 'folder' : 'browser' ,
231237 'categories' : ['playwright' ],
232238 },
233239 {
234240 'id' : 'office-automation' ,
235241 'name' : '업무 자동화' ,
236242 'description' : '워크북, 작은 도구, 반복 업무를 실행 가능한 Python으로 만든다.' ,
243+ 'folder' : 'office' ,
237244 'categories' : ['excel' , 'practical' ],
238245 },
239246 {
240247 'id' : 'text-automation' ,
241248 'name' : '텍스트 자동화' ,
242249 'description' : '비정형 문자열 추출과 변환을 자동화한다.' ,
250+ 'folder' : 'text' ,
243251 'categories' : ['regex' ],
244252 },
245253 {
246254 'id' : 'os-automation' ,
247255 'name' : 'OS 자동화' ,
248256 'description' : '파일, 프로세스, 입력, 창 제어 트랙을 위한 자리다.' ,
257+ 'folder' : 'os' ,
249258 'categories' : [],
250259 },
251260 {
252261 'id' : 'test-automation' ,
253262 'name' : '테스트 자동화' ,
254263 'description' : '단위, 통합, E2E, 회귀 테스트 트랙을 위한 자리다.' ,
264+ 'folder' : 'test' ,
255265 'categories' : [],
256266 },
257267 ],
260270 'id' : 'image-vision' ,
261271 'name' : '이미지·비전' ,
262272 'description' : '이미지 처리와 화면 인식 기반 자동화를 다룬다.' ,
273+ 'folder' : 'imageVision' ,
263274 'categories' : ['pillow' , 'opencv' ],
264275 },
265276]
266277
278+
279+ def _buildCategoryFolderMap (nodes , prefix = '' ):
280+ mapping = {}
281+ for node in nodes :
282+ nodeFolder = node .get ('folder' ) or node .get ('id' ) or ''
283+ currentPrefix = f"{ prefix } /{ nodeFolder } " if prefix else nodeFolder
284+ for category in node .get ('categories' , []) or []:
285+ mapping [category ] = f"{ currentPrefix } /{ category } " if currentPrefix else category
286+ children = node .get ('children' )
287+ if isinstance (children , list ):
288+ mapping .update (_buildCategoryFolderMap (children , currentPrefix ))
289+ return mapping
290+
291+
292+ categoryFolderMap = _buildCategoryFolderMap (categoryTree )
293+
294+
295+ def categoryDirRelative (category : str ) -> str :
296+ return categoryFolderMap .get (category , category )
297+
298+
299+ def getCategoryDir (category : str ) -> Path :
300+ return contentDir / categoryDirRelative (category )
301+
267302featuredCategories = ['30days' , 'pandas' , 'advancedPython' , 'matplotlib' ]
268303
269304learningPaths = {
@@ -346,7 +381,7 @@ def loadContent(category: str, contentId: str, noCache: bool = False) -> dict:
346381 return content
347382
348383def listContents (category : str ) -> list :
349- categoryDir = contentDir / category
384+ categoryDir = getCategoryDir ( category )
350385 if not categoryDir .exists ():
351386 return []
352387 contents = []
@@ -360,14 +395,15 @@ def listContents(category: str) -> list:
360395 return contents
361396
362397def getContentPath (category : str , contentId : str ) -> Path :
398+ categoryDir = getCategoryDir (category )
363399 if category == 'practical' :
364- subDirPath = contentDir / category / contentId / "study.yaml"
400+ subDirPath = categoryDir / contentId / "study.yaml"
365401 if subDirPath .exists ():
366402 return subDirPath
367- return contentDir / category / f"{ contentId } .yaml"
403+ return categoryDir / f"{ contentId } .yaml"
368404
369405def getAllCategories () -> list :
370- return [d . name for d in contentDir . iterdir () if d . is_dir () and d . name != 'renderers' and d . name != '__pycache__' ]
406+ return [category for category in categoryFolderMap if getCategoryDir ( category ). exists () ]
371407
372408def buildMenuTitle (contentId : str , metaTitle : str ) -> str :
373409 dayMatch = re .match (r'day(\d+)(?:_(\d+))?(?:_(.+))?' , contentId )
0 commit comments