From a4861dc1b83d5a80a2383acbc0cd4e718c186fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Sun, 15 Feb 2026 18:31:15 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A1=B0=EC=A0=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=95=BD=EC=9E=AC=20=EC=B6=94=EA=B0=80=20=EC=8B=9C=20?= =?UTF-8?q?=EB=A7=88=EC=8A=A4=ED=84=B0=20=EC=95=BD=EC=9E=AC=EB=AA=85=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B0=8F=202=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 약재 추가 드롭다운에서 제품명 대신 마스터 약재명 표시 - /api/herbs/masters 엔드포인트 사용하여 ingredient_code 기반 약재 목록 로드 - /api/herbs/by-ingredient/ 엔드포인트 추가 (제품 목록 조회) - 2단계 선택 구조: 약재(마스터) → 제품 → 원산지/롯트 - 기존 처방 약재와 새로 추가하는 약재의 테이블 구조 통일 (6칼럼) - 원산지 선택 칼럼에 제품/원산지 드롭다운 함께 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app.py | 34 +++++++++++ static/app.js | 112 ++++++++++++++++++++++++++++++------ test_compound_e2e.py | 131 ++++++++++++++++++++++++++---------------- test_herb_select.html | 43 ++++++++++++++ 4 files changed, 253 insertions(+), 67 deletions(-) create mode 100644 test_herb_select.html diff --git a/app.py b/app.py index 40214c5..76f9118 100644 --- a/app.py +++ b/app.py @@ -255,6 +255,40 @@ def get_herb_masters(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/herbs/by-ingredient/', methods=['GET']) +def get_herbs_by_ingredient(ingredient_code): + """특정 ingredient_code에 해당하는 제품 목록 조회""" + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + h.herb_item_id, + h.insurance_code, + h.herb_name, + h.specification, + CASE + WHEN h.specification LIKE '%신흥%' THEN '신흥' + WHEN h.specification LIKE '%한동%' THEN '한동' + ELSE COALESCE(h.specification, '기타') + END as company_name, + COALESCE(SUM(il.quantity_onhand), 0) as stock_quantity, + COUNT(DISTINCT il.lot_id) as lot_count, + AVG(il.unit_price_per_g) as avg_price + FROM herb_items h + LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id + AND il.is_depleted = 0 + WHERE h.ingredient_code = ? + AND h.is_active = 1 + GROUP BY h.herb_item_id + ORDER BY stock_quantity DESC, h.herb_name + """, (ingredient_code,)) + + products = [dict(row) for row in cursor.fetchall()] + return jsonify({'success': True, 'data': products}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + # ==================== 처방 관리 API ==================== @app.route('/api/formulas', methods=['GET']) diff --git a/static/app.js b/static/app.js index 5f55b53..dcb1f10 100644 --- a/static/app.js +++ b/static/app.js @@ -617,15 +617,15 @@ $(document).ready(function() { value="${ing.grams_per_cheop}" min="0.1" step="0.1"> ${totalGrams.toFixed(1)} - - - - +
+ + +
대기중 @@ -722,7 +722,7 @@ $(document).ready(function() { // 빈 약재 행 추가 함수 function addEmptyIngredientRow() { const newRow = $(` - + - - +
+ + +
- @@ -750,18 +755,53 @@ $(document).ready(function() { $('#compoundIngredients').append(newRow); // 약재 목록 로드 - loadHerbsForSelect(newRow.find('.herb-select-compound')); + const herbSelect = newRow.find('.herb-select-compound'); + loadHerbsForSelect(herbSelect); - // 약재 선택 시 원산지 옵션 로드 + // 약재(마스터) 선택 시 제품 옵션 로드 newRow.find('.herb-select-compound').on('change', function() { - const herbId = $(this).val(); - if (herbId) { + const ingredientCode = $(this).val(); + const herbName = $(this).find('option:selected').data('herb-name'); + if (ingredientCode) { const row = $(this).closest('tr'); + row.attr('data-ingredient-code', ingredientCode); + + // 제품 목록 로드 + loadProductOptions(row, ingredientCode, herbName); + + // 제품 선택 활성화 + row.find('.product-select').prop('disabled', false); + + // 원산지 선택 초기화 및 비활성화 + row.find('.origin-select').empty().append('').prop('disabled', true); + } else { + const row = $(this).closest('tr'); + row.attr('data-ingredient-code', ''); + row.attr('data-herb-id', ''); + row.find('.product-select').empty().append('').prop('disabled', true); + row.find('.origin-select').empty().append('').prop('disabled', true); + } + }); + + // 제품 선택 이벤트 + newRow.find('.product-select').on('change', function() { + const herbId = $(this).val(); + const row = $(this).closest('tr'); + + if (herbId) { row.attr('data-herb-id', herbId); + + // 원산지 선택 활성화 + row.find('.origin-select').prop('disabled', false); + + // 원산지 옵션 로드 const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0; const totalGrams = gramsPerCheop * cheopTotal; loadOriginOptions(herbId, totalGrams); + } else { + row.attr('data-herb-id', ''); + row.find('.origin-select').empty().append('').prop('disabled', true); } }); @@ -1599,14 +1639,48 @@ $(document).ready(function() { } function loadHerbsForSelect(selectElement) { - $.get('/api/herbs', function(response) { + $.get('/api/herbs/masters', function(response) { if (response.success) { selectElement.empty().append(''); - response.data.forEach(herb => { - selectElement.append(``); + // 재고가 있는 약재만 필터링하여 표시 + const herbsWithStock = response.data.filter(herb => herb.has_stock === 1); + + herbsWithStock.forEach(herb => { + // ingredient_code를 value로 사용하고, 한글명(한자명) 형식으로 표시 + let displayName = herb.herb_name; + if (herb.herb_name_hanja) { + displayName += ` (${herb.herb_name_hanja})`; + } + selectElement.append(``); }); } + }).fail(function(error) { + console.error('Failed to load herbs:', error); + }); + } + + // ingredient_code 기반으로 제품 옵션 로드 + function loadProductOptions(row, ingredientCode, herbName) { + $.get(`/api/herbs/by-ingredient/${ingredientCode}`, function(response) { + if (response.success) { + const productSelect = row.find('.product-select'); + productSelect.empty(); + + if (response.data.length === 0) { + productSelect.append(''); + productSelect.prop('disabled', true); + } else { + productSelect.append(''); + response.data.forEach(product => { + const stockInfo = product.stock_quantity > 0 ? `(재고: ${product.stock_quantity.toFixed(1)}g)` : '(재고 없음)'; + productSelect.append(``); + }); + productSelect.prop('disabled', false); + } + } + }).fail(function() { + console.error(`Failed to load products for ingredient code: ${ingredientCode}`); }); } diff --git a/test_compound_e2e.py b/test_compound_e2e.py index bb7190d..5b6931a 100644 --- a/test_compound_e2e.py +++ b/test_compound_e2e.py @@ -35,6 +35,14 @@ def test_compound_ginseng_selection(): time.sleep(2) print("✓ 조제관리 화면 진입") + # 조제 입력 섹션 표시 + print("\n[2-1] 조제 입력 섹션 표시...") + show_compound_entry = page.locator('#showCompoundEntry') + if show_compound_entry.count() > 0: + show_compound_entry.click() + time.sleep(1) + print("✓ 조제 입력 섹션 표시") + # 3. 현재 화면 상태 확인 print("\n[3] 화면 상태 확인...") @@ -54,10 +62,16 @@ def test_compound_ginseng_selection(): # 처방 선택 시도 print("\n[4] 처방 선택...") - # 처방 드롭다운 찾기 (유연하게) - formula_select = page.locator('select').first + # compoundFormula select 요소 찾기 (ID로 정확히) + formula_select = page.locator('#compoundFormula') if formula_select.count() > 0: + # select가 visible 될 때까지 기다리기 + try: + formula_select.wait_for(state="visible", timeout=5000) + except: + print("⚠️ 처방 선택 드롭다운이 보이지 않음") + # 옵션 확인 options = formula_select.locator('option').all() print(f"✓ 드롭다운 옵션: {len(options)}개") @@ -71,72 +85,93 @@ def test_compound_ginseng_selection(): print("✓ 쌍화탕 선택 완료") except Exception as e: print(f"⚠️ label로 선택 실패: {e}") - # index로 시도 - formula_select.select_option(index=1) - time.sleep(3) - print("✓ 첫 번째 처방 선택 완료") + # index로 시도 (첫 번째 옵션은 보통 placeholder이므로 index=1) + try: + formula_select.select_option(index=1) + time.sleep(3) + print("✓ 첫 번째 처방 선택 완료") + except Exception as e2: + print(f"❌ 처방 선택 실패: {e2}") else: print("❌ 처방 드롭다운을 찾을 수 없음") - # 4. 약재 목록 확인 - print("\n[4] 약재 목록 확인...") + # 5. 약재 추가 버튼 클릭 + print("\n[5] 약재 추가 버튼 클릭...") - # 약재 테이블이나 목록이 나타날 때까지 대기 - page.wait_for_selector('table, .ingredient-list', timeout=10000) + # 약재 추가 버튼 찾기 + add_ingredient_btn = page.locator('#addIngredientBtn') - # 페이지 스크린샷 - page.screenshot(path='/tmp/compound_screen_1.png') - print("✓ 스크린샷 저장: /tmp/compound_screen_1.png") + if add_ingredient_btn.count() > 0: + add_ingredient_btn.click() + time.sleep(1) + print("✓ 약재 추가 버튼 클릭 완료") - # 5. 인삼 항목 찾기 - print("\n[5] 인삼 항목 찾기...") + # 6. 새로 추가된 행에서 약재 선택 드롭다운 확인 + print("\n[6] 약재 선택 드롭다운 확인...") - # 인삼을 포함하는 행 찾기 - ginseng_row = page.locator('tr:has-text("인삼"), div:has-text("인삼")').first + # 새로 추가된 행 찾기 (마지막 행) + new_row = page.locator('#compoundIngredients tr').last - if ginseng_row.count() > 0: - print("✓ 인삼 항목 발견") + # 약재 선택 드롭다운 찾기 + herb_select = new_row.locator('.herb-select-compound') - # 6. 제품 선택 드롭다운 확인 - print("\n[6] 제품 선택 드롭다운 확인...") + if herb_select.count() > 0: + print("✓ 약재 선택 드롭다운 발견") - # 인삼 행에서 select 요소 찾기 - product_select = ginseng_row.locator('select').first + # 드롭다운 옵션 확인 + time.sleep(1) # 드롭다운이 로드될 시간 확보 + options = herb_select.locator('option').all() + print(f"✓ 약재 옵션: {len(options)}개") - if product_select.count() > 0: - print("✓ 제품 선택 드롭다운 발견") - - # 옵션 개수 확인 - options = product_select.locator('option').all() - print(f"✓ 사용 가능한 제품: {len(options)}개") - - # 각 옵션 출력 - for idx, option in enumerate(options): + # 처음 10개 옵션 출력 + for idx, option in enumerate(options[:10]): text = option.text_content() value = option.get_attribute('value') print(f" [{idx}] {text} (value: {value})") - # 신흥인삼 또는 세화인삼 선택 가능한지 확인 - has_shinheung = any('신흥인삼' in opt.text_content() for opt in options) - has_sehwa = any('세화인삼' in opt.text_content() for opt in options) + # 마스터 약재명이 표시되는지 확인 + has_master_names = False + for option in options: + text = option.text_content() + # ingredient_code 형식의 value와 한글/한자 형식의 텍스트 확인 + if '(' in text and ')' in text: # 한자 포함 형식 + has_master_names = True + break - if has_shinheung or has_sehwa: - print("\n✅ 인삼 제품 선택 가능!") + if has_master_names: + print("\n✅ 마스터 약재명이 드롭다운에 표시됨!") - # 첫 번째 제품 선택 시도 - if len(options) > 0: - product_select.select_option(index=0) - print(f"✓ '{options[0].text_content()}' 선택 완료") + # 인삼 선택 시도 + try: + herb_select.select_option(label='인삼 (人蔘)') + print("✓ 인삼 선택 완료") + except: + # label이 정확히 일치하지 않으면 부분 매칭 + for idx, option in enumerate(options): + if '인삼' in option.text_content(): + herb_select.select_option(index=idx) + print(f"✓ 인삼 선택 완료 (index {idx})") + break + + time.sleep(1) + + # 제품 선택 드롭다운 확인 + product_select = new_row.locator('.product-select') + if product_select.count() > 0: + print("\n[7] 제품 선택 드롭다운 확인...") + time.sleep(1) # 제품 목록 로드 대기 + + product_options = product_select.locator('option').all() + print(f"✓ 제품 옵션: {len(product_options)}개") + for idx, option in enumerate(product_options): + print(f" [{idx}] {option.text_content()}") else: - print("\n❌ 인삼 대체 제품이 드롭다운에 없음") + print("\n⚠️ 마스터 약재명 대신 제품명이 드롭다운에 표시됨") + print("(신흥생강, 신흥작약 등의 제품명이 보임)") else: - print("❌ 제품 선택 드롭다운을 찾을 수 없음") - print("페이지 HTML 일부:") - print(ginseng_row.inner_html()[:500]) + print("❌ 약재 선택 드롭다운을 찾을 수 없음") else: - print("❌ 인삼 항목을 찾을 수 없음") - print("\n페이지 내용:") - print(page.content()[:2000]) + print("❌ 약재 추가 버튼을 찾을 수 없음") # 7. 최종 스크린샷 page.screenshot(path='/tmp/compound_screen_final.png') diff --git a/test_herb_select.html b/test_herb_select.html new file mode 100644 index 0000000..f39815b --- /dev/null +++ b/test_herb_select.html @@ -0,0 +1,43 @@ + + + + 약재 선택 테스트 + + + +

약재 선택 드롭다운 테스트

+ +
+ + + + + + \ No newline at end of file