fix: 조제 관리 약재 추가 시 마스터 약재명 표시 및 2단계 선택 구조 개선
- 약재 추가 드롭다운에서 제품명 대신 마스터 약재명 표시 - /api/herbs/masters 엔드포인트 사용하여 ingredient_code 기반 약재 목록 로드 - /api/herbs/by-ingredient/<code> 엔드포인트 추가 (제품 목록 조회) - 2단계 선택 구조: 약재(마스터) → 제품 → 원산지/롯트 - 기존 처방 약재와 새로 추가하는 약재의 테이블 구조 통일 (6칼럼) - 원산지 선택 칼럼에 제품/원산지 드롭다운 함께 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bfc5c992de
commit
a4861dc1b8
34
app.py
34
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/<ingredient_code>', 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'])
|
||||
|
||||
102
static/app.js
102
static/app.js
@ -617,15 +617,15 @@ $(document).ready(function() {
|
||||
value="${ing.grams_per_cheop}" min="0.1" step="0.1">
|
||||
</td>
|
||||
<td class="total-grams">${totalGrams.toFixed(1)}</td>
|
||||
<td class="product-select-cell">
|
||||
<select class="form-control form-control-sm product-select" ${ing.available_products.length === 0 ? 'disabled' : ''}>
|
||||
<td class="origin-select-cell">
|
||||
<div class="d-flex gap-1">
|
||||
<select class="form-control form-control-sm product-select" style="flex: 1;" ${ing.available_products.length === 0 ? 'disabled' : ''}>
|
||||
${productOptions}
|
||||
</select>
|
||||
</td>
|
||||
<td class="origin-select-cell">
|
||||
<select class="form-control form-control-sm origin-select" disabled>
|
||||
<select class="form-control form-control-sm origin-select" style="flex: 1;" disabled>
|
||||
<option value="">제품 먼저 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td class="stock-status">대기중</td>
|
||||
<td>
|
||||
@ -722,7 +722,7 @@ $(document).ready(function() {
|
||||
// 빈 약재 행 추가 함수
|
||||
function addEmptyIngredientRow() {
|
||||
const newRow = $(`
|
||||
<tr>
|
||||
<tr data-ingredient-code="" data-herb-id="">
|
||||
<td>
|
||||
<select class="form-control form-control-sm herb-select-compound">
|
||||
<option value="">약재 선택</option>
|
||||
@ -734,9 +734,14 @@ $(document).ready(function() {
|
||||
</td>
|
||||
<td class="total-grams">0.0</td>
|
||||
<td class="origin-select-cell">
|
||||
<select class="form-control form-control-sm origin-select" disabled>
|
||||
<div class="d-flex gap-1">
|
||||
<select class="form-control form-control-sm product-select" style="flex: 1;" disabled>
|
||||
<option value="">약재 선택 후 표시</option>
|
||||
</select>
|
||||
<select class="form-control form-control-sm origin-select" style="flex: 1;" disabled>
|
||||
<option value="">제품 선택 후 표시</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td class="stock-status">-</td>
|
||||
<td>
|
||||
@ -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('<option value="">제품 선택 후 표시</option>').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('<option value="">약재 선택 후 표시</option>').prop('disabled', true);
|
||||
row.find('.origin-select').empty().append('<option value="">제품 선택 후 표시</option>').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('<option value="">제품 선택 후 표시</option>').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('<option value="">약재 선택</option>');
|
||||
|
||||
response.data.forEach(herb => {
|
||||
selectElement.append(`<option value="${herb.herb_item_id}">${herb.herb_name}</option>`);
|
||||
// 재고가 있는 약재만 필터링하여 표시
|
||||
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(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}">${displayName}</option>`);
|
||||
});
|
||||
}
|
||||
}).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('<option value="">재고 없음</option>');
|
||||
productSelect.prop('disabled', true);
|
||||
} else {
|
||||
productSelect.append('<option value="">제품 선택</option>');
|
||||
response.data.forEach(product => {
|
||||
const stockInfo = product.stock_quantity > 0 ? `(재고: ${product.stock_quantity.toFixed(1)}g)` : '(재고 없음)';
|
||||
productSelect.append(`<option value="${product.herb_item_id}" ${product.stock_quantity === 0 ? 'disabled' : ''}>${product.company_name} ${stockInfo}</option>`);
|
||||
});
|
||||
productSelect.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
}).fail(function() {
|
||||
console.error(`Failed to load products for ingredient code: ${ingredientCode}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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로 시도
|
||||
# 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')
|
||||
|
||||
43
test_herb_select.html
Normal file
43
test_herb_select.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>약재 선택 테스트</title>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>약재 선택 드롭다운 테스트</h1>
|
||||
|
||||
<div id="test-area"></div>
|
||||
|
||||
<button id="add-row">행 추가</button>
|
||||
|
||||
<script>
|
||||
$('#add-row').click(function() {
|
||||
// API 호출하여 약재 목록 가져오기
|
||||
$.get('http://localhost:5001/api/herbs/masters', function(response) {
|
||||
console.log('API Response:', response);
|
||||
|
||||
if (response.success) {
|
||||
const select = $('<select></select>');
|
||||
select.append('<option value="">약재 선택</option>');
|
||||
|
||||
// 재고가 있는 약재만 필터링
|
||||
const herbsWithStock = response.data.filter(herb => herb.has_stock === 1);
|
||||
console.log('Herbs with stock:', herbsWithStock.length);
|
||||
|
||||
herbsWithStock.forEach(herb => {
|
||||
let displayName = herb.herb_name;
|
||||
if (herb.herb_name_hanja) {
|
||||
displayName += ` (${herb.herb_name_hanja})`;
|
||||
}
|
||||
select.append(`<option value="${herb.ingredient_code}">${displayName}</option>`);
|
||||
});
|
||||
|
||||
$('#test-area').append(select);
|
||||
$('#test-area').append('<br><br>');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user