묻고 답해요
160만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결한국인이 좋아하는 속도로 때려넣는 파이썬
보조 기억 장치와 보조기억장치가 바뀐거 같습니다.
[프로그램 설치 오류 문의]프로그램 설치 오류 발생시 사용중인 컴퓨터의 환경에 대한 상세 스크린샷을 함께 제공해 주셔야 합니다!또한, 강의에서 소개한 내용과 완전히 동일한 방식으로 설치를 진행하셨는지도 재확인 부탁드려요! [코드 실행 오류 문의]반드시 코드와 에러메시지의 스크린샷을 첨부해 주셔야 합니다!둘 중 하나라도 누락되면 도움을 드릴 수 없습니다! [그 외 오류 문의]그 외 오류가 발생한 경우 최대한 스크린샷이나 코드 등, 상황을 전달할 수 있는 자료를 수집하시어 함께 제공해 주세요.
-
미해결Airflow 마스터 클래스
macOS에서 docker 설치
안녕하세요 mac에서 docker 설치하려고 하는데 apt-get command not found가 뜹니다. 찾아보니 apt 명령어는 macOS에서 동작 안한다고 하는데 어떻게 진행하면 될까요?
-
해결됨(2025) 일주일만에 합격하는 정보처리기사 실기
커리큘럼문의
언어과목에서 다 맞는 목표가 아니면 커리큘럼 중에 섹션 7, 10, 12는 안봐도 될까요?
-
해결됨남박사의 파이썬으로 실전 웹사이트 만들기
검색기능 질문
from flask import Flask from flask import request from flask import render_template from flask_pymongo import PyMongo from datetime import datetime from bson.objectid import ObjectId from flask import abort from flask import redirect from flask import url_for import time import math app = Flask(__name__) app.config["MONGO_URI"] = "mongodb://localhost:27017/myweb" mongo = PyMongo(app) @app.template_filter("formatdatetime") def format_datetime(value): if value is None: return "" now_timestamp = time.time() offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp(now_timestamp) value = datetime.fromtimestamp((int(value) / 1000)) + offset return value.strftime('%Y-%m-%d %H:%M:%S') @app.route("/list") def lists(): #페이지 값(값이 없는 경우 기본값은 1) page = request.args.get("page", 1, type=int) #한페이지당 몇개의 게시물을 출력할지 limit = request.args.get("limit", 7, type = int) search = request.args.get("search", -1, type=int) keyword = request.args.get("keyword", type=str) #최종적으로 완성된 쿼리를 만들 변수 query = {} #검색어 상태를 추가할 리스트 변수 search_list = [] if search == 0: search_list.append({"title":{"$regex": keyword}}) elif search ==1: search_list.append({"contents":{"$regex": keyword}}) elif search ==2: search_list.append({"title":{"$regex": keyword}}) search_list.append({"contents":{"$regex": keyword}}) elif search == 3: search_list.append({"name":{"$regex": keyword}}) #검색 대상이 한개라도 존재할 경우 query 변수에 $or 리스트를 쿼리 합니다. if len(search_list) > 0: query = {"$or": search_list} print(query) board = mongo.db.board datas = board.find(query).skip((page - 1)*limit).limit(limit) #게시물의 총 갯수 tot_count = board.find(query).count() #마지막 페이지의 수를 구합니다. last_page_num = math.ceil(tot_count / limit) #페이지 블럭을 5개씩 표기 block_size = 5 #현재 블럭의 위치 block_num=int((page-1)/ block_size) #블럭의 시작 위치 block_start = int((block_size * block_num) +1) #블럭의 끝 위치 block_last = math.ceil(block_start +(block_size - 1)) return render_template( "list.html", datas = datas, limit=limit, page=page, block_start=block_start, block_last=block_last, last_page_num=last_page_num, search=search, keyword=keyword) @app.route("/view/<idx>") def board_view(idx): # idx = request.args.get("idx") if idx is not None: board = mongo.db.board data = board.find_one({"_id": ObjectId(idx)}) if data is not None: result = { "id" : data.get("_id"), "name" : data.get("name"), "title" : data.get("title"), "contents" : data.get("contents"), "pubdate" : data.get("pubdate"), "view" : data.get("view"), } return render_template("view.html", result=result) return abort(400) @app.route("/wirte", methods = ["GET", "POST"]) def board_wirte(): if request.method == "POST": name = request.form.get("name") title = request.form.get("title") contents = request.form.get("contents") print(name, title, contents) current_utc_time = round(datetime.utcnow().timestamp() * 1000) board = mongo.db.board post = { "name" : name, "title" : title, "contents" : contents, "pubdate" : current_utc_time, "view" : 0, } x = board.insert_one(post) print(x.inserted_id) return redirect(url_for("board_view", idx=x.inserted_id)) else: return render_template("wirte.html") if __name__ == "__main__": app.run(debug = True) <script> function search() { var v_search = document.getElementById("search").value; var v_keyword = document.getElementById("keyword").value; if(v_search == "" || v_keyword == ""){ return false; } else{ self.location.href="{{url_for('lists')}}?search=" + v_search + "&keyword=" + v_keyword; } } </script> {% if datas.count() > 0%} <table> <thead> <tr> <td>번호</td> <td>제목</td> <td>이름</td> <td>날짜</td> <td>조회수</td> </tr> </thead> <tbody> <!--반복되는 구간--> {%for data in datas%} <tr> <td>{{loop.index + (page - 1) * limit}}</td> <td><a href="{{url_for('board_view', idx=data._id, page=page, search=search, keyword=keyword)}}">{{data.title}}</a></td> <td>{{data.name}}</td> <td>{{data.pubdate | formatdatetime}}</td> <td>{{data.view}}</td> </tr> <!--반복되는 구간 끝--> {%endfor%} </tbody> </table> {%if block_start -1 > 0%} <a href="{{url_for('lists', page=block_start - 1, search=search, keyword=keyword)}}">[이전]</a> {%endif%} {%for i in range(block_start, block_last + 1)%} {% if i > last_page_num%} {%else%} {%if i == page%} <b>{{i}}</b> {%else%} <a href="{{url_for('lists', page=i, search=search, keyword=keyword)}}">{{i}}</a> {%endif%} {%endif%} {%endfor%} {%if block_last < last_page_num%} <a href="{{url_for('lists', page=block_last+ 1, search=search, keyword=keyword)}}">[다음]</a> {%endif%} <select name="search" id="search"> <option value="">검색대상</option> <option value="0">제목</option> <option value="1">내용</option> <option value="2">제목 + 내용</option> <option value="3">작성자</option> </select> <input type="text" name="keyword" id="keyword"> <input type="button" value="검색" onclick="search()"> {% else %} <h3>데이터가 없습니다.</h3> {%endif%} 분명 똑같이 했는데 왜 검색이 안되고 무반응인지 잘 모르겠어요. 뭐가 문제일까요?
-
미해결Airflow 마스터 클래스
템플릿 변수에 대한 오류
안녕하세요 CLI로 Trigger 기능을 수행하는 부분 강의를 듣던 중에,Web UI에서 Trigger를 누르면 정상적으로 수행되지만,쉘 스크립트 커맨드로 airflow dags trigger <DAG 이름>이라는 명령어를 실행했을 때아래와 같은 실패 로그가 나타나서 문의드립니다.혹시 커맨드라인으로 실행하면 {{data_interval_end}} 와 같은 템플릿을 적용할 수 없나요?
-
해결됨실전! Django 입문 [최신 5.2 버전]
ConnectionRefusedError: [WinError 10061]
ConnectionRefusedError: [WinError 10061] Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte에러가 발생했는데 찾아보니 EMAIL_USE_TLS = False EMAIL_USE_SSL = True EMAIL_PORT = 465위의 코드로도 시도를 해보라해서 해봤지만 계속 동일한 에러가 발생했습니다.강의에서 사용된 운영체제는 맥 os이고 저는 윈도우 환경에서 실행했는데 윈도우환경에서 자주 발생하는 에러라고 하는데 해결방법을 찾기가 어려워서 문의남깁니다.
-
미해결실전! Django 입문 [최신 5.2 버전]
CSRF_COOKIE_NAME에러
settings.CSRF_COOKIE_NAME이라는게 존재하지 않는다고 나와요
-
해결됨(2025) 일주일만에 합격하는 정보처리기사 실기
제본
제본을하려고 합니다!총 매수가 어느정도 나오나요??
-
해결됨남박사의 파이썬으로 실전 웹사이트 만들기
google.py
강사님께서 알려주신 from googlesearch import search query = "파이썬 강좌" for r in search(query, num_results=10, advanced=True): title = r.title contents = r.description url = r.url print(f"Title: {title}") print(f"Contents: {contents}") print(f"URL: {url}") print("-" * 40)요 코드대로 해보았으나출력이 안됩니다. 뭐가문제일까요?? ''' 게시판 리스트 테스트를 위한 Google 검색어 크롤링 프로그램 설치 해야하는 라이브러리 pip install --upgrade BeautifulSoup4 requests lxml pymongo googlesearch-python ''' import requests from bs4 import BeautifulSoup from pymongo import MongoClient from datetime import datetime import random from googlesearch import search # 몽고DB client = MongoClient(host="localhost", port=27017) # myweb 데이터베이스 db = client.myweb # board 컬렉션 col = db.board # 구글 검색시 헤더값을 설저하지 않으면 브라우저에서 보이는것과 다른 결과가 나옴 header = {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"} # 검색 결과 30개 수집 query = "파이썬 강좌" for r in search(query, num_results=30, advanced=True): # 구글 검색 URL, 검색어는 파이썬 # 게시물 작성시간 기록을 위해 현재시간 저장 (utc 타임) current_utc_time = round(datetime.utcnow().timestamp() * 1000) try: title = r.title contents = r.description # 몽고DB에 저장 # 작성자와 writer_id 설정 필요 col.insert_one({ "name": "테스터", "writer_id": "", "title": title, "contents": contents, "view": random.randrange(30, 777), "pubdate": current_utc_time }) except: pass 이것도 수업자료에 있었던 코드입니다. 이대로 해도 진행이 안되네요..
-
해결됨(2025) 일주일만에 합격하는 정보처리기사 실기
1:03:00 node
func 매서드에서 while문 마지막 실행문인node = node -> next -> next; 가 이해가 잘 안되었는데요.해당 실행문 위에 있는 다른 실행문은 node->value 또는 node->next를 통해서 n1메모리에 참조해서 그 값을 가르키기 때문에 =을 통해 값변화가 일어난다.하지만 node = node -> next -> next; 의 경우에 node는 func메서드가 가지고 있는 지역변수이기 때문에 main에서부터 참조받은 n1 변수를 변경하지 않는다. 이게 맞을까요?저말고도 다른분들도 여럿 같은 질문을 했는데 정확한 답변이 없어서 제가 이해한게 맞는지 올려봅니다.. 그럼에도 확실하게 개념이 잡히지 않네요. node -> value 또는 node -> next조차도 func메서드 안에 있는 지역변수의 요소들인 건 매한가지 아닌가요...ㅠㅠ node도 메모리주소.. node->next->next도 메모리주소인데.. 왜 지역변수로 얕은복사(?)가 되는 것인지..ㅠㅠ
-
해결됨(2025) 일주일만에 합격하는 정보처리기사 실기
35q분
comp 메서드에 매개변수가 들어갈 때, str.length-1로 들어가면 뒤에서부터 재귀함수가 시작해서 d부터 들어가고 c b a가 순서대로 들어가야하는 것 처럼 보이는데요.이때, if문 안에서 result = c + result 로 되어 있는데요. result에 이미 d가 들어가있다고 가정했을 때 식이 진행되면 result = "cd"가 되는 것인지 result = "dc"가 되는 것인지 궁금합니다. 만약 전자라면 정답이 abcd가 되어야 하는 것이 아닌가요? index 마지막부터 시작되니까요.
-
해결됨(2025) 일주일만에 합격하는 정보처리기사 실기
정보처리기사 30일 플랜을 짜고 싶습니다.
안녕하세요 주말코딩님. 선생님의 강의를 듣고있는 정보처리기사 실기를 수강하고 있는 학생입니다.1회차는 떨어졌고, 2회차는 사정 상 접수를 못하게 되어 3회차에 접수하여 응시를 할 생각입니다. 1회차는 아마 40점정도로 맞은 것 같습니다. 이론은 대부분 맞혔지만 코드에서 대부분 정답을 맞히지 못해 불합격을 하고 말았습니다. 선생님의 강의를 30일플랜에 맞춰서 활용하려고 하는데 몇 가지 여쭙고 싶은 게 있어 질문을 드리고자 합니다. 3회차 시험 날까지 30일정도 시간이 있으며, 낮에는 회사에 근무하고 있어서 공부 시간은 저녁 7시부터 가능한 상황입니다. 주말에는 가정사가 있어 1-2시간정도만 학습이 가능한 상황입니다. 시험을 대비하기에 충분한 시간이 될 수 있을까요? 이론은 최근 나온 기출문제에 나온 문제들로 이미 틈틈이 학습 중입니다. 하지만 문제가 코드를 푸는 문제에 있어서 걱정이 됩니다. 일단 코드 문제를 풀 때 main부터 차근차근 풀어나갈 때, 어딘가 반드시 막히게 되더라구요.. 기초는 있지만 응용이 잘 안되는 것 같습니다. 혹시, 정보처리기사 실기문제를 ide상에서 복사해서 붙여 넣지 않고 직접 코딩을 해보는 것이 이해에 도움이 될까요?30일 플랜을 짤 때 어떻게 짜면 좋을까요? "1주차에는 몇 강을 듣고 몇 강을 들어라"이런 식으로 조언 부탁 드립니다.바쁘시더라도 답변 부탁 드리겠습니다.감사합니다..
-
해결됨38군데 합격 비법, 2025 코딩테스트 필수 알고리즘
교재 수강평 인증 방법
수강평 인증 후 취업 및 교재 관련 정보 알려주신다고 소개글에 나와있었는데, 맨 마지막으로 가도 못찾겠어서요!! 어디서 얻을 수 있을까요?
-
미해결파이썬 무료 강의 (기본편) - 6시간 뒤면 나도 개발자
2장 환경설정 문의
2장 환경설정 Visual Studio Code에서 64비트라 그런지 화면 버전과 다릅니다. print("Hello world") 입력 후 벌레모양 눌리면 Debug with Python이 안보입니다.
-
미해결퇴근 2시간 당기는 자동화, 코딩 몰라도 됩니다
n8n 설치 에러
설치 시 에러 발생했는데 어떤게 문제일까요..ㅠ
-
미해결파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 (장고 4.2 기준)
Django의 View나 URL의 네이밍 컨벤션
안녕하세요 강사님, 스타트업에서 django를 활용해 개발하고 있는 신입 개발자입니다.일을 시작하다 보니 제가 django에 대해 잘 모르고 있다는 것을 느껴 수강하게 되었는데요, 그 중 가장 고민이 되는 부분이 네이밍이었습니다.그래서 질문드리는 것이 Django에서는 View나 URL 엔드포인트 별명(name=에 들어가는 값)을 지을 때 해당 객체의 타입도 같이 적는지 궁금합니다.예를 들어 View의 이름을 지을 때 PostRenderView라고 짓는지, 아니면 PostRender라고만 해도 될까요? url name을 지을 때에도 post_render라고만 하면 되는지, post_render_view라고 해주는게 좋은지 django의 컨벤션을 알고 싶습니다.제가 생각하기엔 View의 클래스 이름은 view라는 것을 명확하게 하기 위해 View를 붙이는 게, url name은 template에서 쓰이는 별명이므로 post_render라고만 하는 게 좋다고 생각해서 지금까지 이렇게 짓고 있었습니다.또 HTTP response에 대해 들었던 내용 중에 정확하진 않지만 메서드 타입이 get이면 이미 타입에 역할이 포함되어 있기 때문에 메서드 이름에 get을 넣지 않는 게 좋다라고 들었던 기억이 있어서요. 강사님의 생각이 궁금합니다. 알찬 강의 잘 수강하고 있습니다. 감사합니다!
-
미해결베개 투자법: 자면서 돈 버는 AI 주식 자동 매매 머신
[베개투자법 :자면서 돈버는 AI주식 자동매매머신] 종목 변경시 코드 수정 부문을 알려주세요
안녕하세요. 주식 종목을 변경하고 싶은데 변경해야할 코드 부분을 알려주세요.
-
미해결Airflow 마스터 클래스
custom_image 디렉토리 문의드립니다.
안녕하세요.섹션 12 강의를 듣는중인데 airflow 디렉토리 밑에 custom_image 디렉토리가 이미 하나 있어야 하더라구요.그런데 제 airflow 디렉토리 밑에는 해당 디렉토리가 없습니다.여태 진행한 강의는 분명 빠짐없이 들었는데 제가 실수로 놓친 부분이 있는 것 같습니다..다시 찾아 듣고자 하는데 어느 강의인지 찾지를 못하고 있습니다.죄송하지만 혹시 해당 부분 몇 강에서 진행하셨는지 알 수 있을까요?
-
해결됨38군데 합격 비법, 2025 코딩테스트 필수 알고리즘
재귀함수-2: 회문검사
1. 현재 학습 진도23. 2-9 재귀함수 - 2 2. 어려움을 겪는 부분재귀함수-2 의 영상 6분 쯤에 "for i in range(n)" 부분이 이해가 안 가서요!문자열 길이만큼, 끝까지 연산해야하는 로직인가요?3. 시도해보신 내용range(n)이 range(n/2) 이런식으로 절반까지만 연산해야하는게 아닌가 헷갈려서 질문드립니다 ㅠㅠ! 이렇게 구체적으로 알려주시면, 더 정확하고 도움이 되는 답변을 드릴 수 있습니다! 😊
-
미해결Python 엑셀 프로그래밍 - with xlsxwriter
메일보내기 할 때 에러..
한글파일명인 액셀파일을 첨부하는 것은 안되나요?아니면, 액셀 내 columns value 값이 한글인 것을 가져와서 메일 헤더나 파일명으로 사용한게 에러인 것인지..궁금합니다... 메일주소와 password 는 '^^^^^' 임의 처리한 코딩입니다. 오류내용:❌ 이메일 발송 실패: 진리종합상사 - 'ascii' codec can't encode characters in position 5-7: ordinal not in range(128) import pandas as pd import os from openpyxl.utils import get_column_letter from openpyxl.styles import Font, PatternFill, Alignment, Border, Side import smtplib from email.message import EmailMessage import ssl from email.header import Header from email.utils import formataddr # ------------------------- # 설정 # ------------------------- cost_manage_path = 'cost_manage.xlsx' partner_path = 'partner.xlsx' output_dir = 'output' SENDER_EMAIL = '^^^^^^^^^^^^^^' SENDER_PASSWORD = '^^^^^^^^^^^^^^' SMTP_SERVER = 'smtp.gmail.com' SMTP_PORT = 587 # ------------------------- # 엑셀 읽기 # ------------------------- try: cost_data_df = pd.read_excel(cost_manage_path, sheet_name='Sheet1', header=1) partner_sheet1_df = pd.read_excel(partner_path, sheet_name='Sheet1') partner_sheet2_df = pd.read_excel(partner_path, sheet_name='Sheet2') except FileNotFoundError as e: print(f"오류: 파일을 찾을 수 없습니다. {e.filename}") exit() # ------------------------- # 데이터 전처리 # ------------------------- for df in [cost_data_df, partner_sheet1_df, partner_sheet2_df]: df.columns = df.columns.str.strip() cost_data_df = cost_data_df.drop(columns=['협력사ID', '협력사명']) cost_data_df['상세분류ID'] = cost_data_df['상세분류ID'].astype(str).str.strip() partner_sheet1_df['상세분류ID'] = partner_sheet1_df['상세분류ID'].astype(str).str.strip() partner_sheet2_df['협력사KEY'] = partner_sheet2_df['협력사KEY'].astype(str).str.strip() merged_df = pd.merge(cost_data_df, partner_sheet1_df, on='상세분류ID', how='left') final_df = pd.merge(merged_df, partner_sheet2_df, on='협력사KEY', how='left') final_df = final_df.drop(columns=['상세분류명_y']) final_df = final_df.rename(columns={'상세분류명_x': '상세분류명'}) final_columns = ['상세분류ID', '상세분류명', '협력사KEY', '협력사ID', '협력사명', '상품ID', '규격', '단가', 'moq', '이메일주소'] real_final_df = final_df[final_columns] grouped_by_partner = real_final_df.groupby('협력사KEY') if not os.path.exists(output_dir): os.makedirs(output_dir) # ------------------------- # 엑셀 생성 & 이메일 발송 # ------------------------- for partner_key, group_df in grouped_by_partner: # 엑셀 데이터 준비 columns_for_excel = ['상세분류ID', '상세분류명', '협력사ID', '협력사명', '상품ID', '규격', '단가', 'moq'] data_to_write = group_df[columns_for_excel].copy() header_row1 = pd.DataFrame([['', '', '', '', '', '', '▼작성', '▼작성']], columns=columns_for_excel) header_row2 = pd.DataFrame([columns_for_excel], columns=columns_for_excel) final_excel_df = pd.concat([header_row1, header_row2, data_to_write], ignore_index=True) # 파일 경로 file_name = f"{partner_key}_견적_요청서.xlsx" output_path = os.path.join(output_dir, file_name) # 엑셀 생성 with pd.ExcelWriter(output_path, engine='openpyxl') as writer: final_excel_df.to_excel(writer, index=False, header=False, sheet_name='Sheet1') worksheet = writer.sheets['Sheet1'] worksheet.sheet_view.showGridLines = False # 스타일 설정 bold_yellow_fill = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid') center_align = Alignment(horizontal='center') left_align = Alignment(horizontal='left') bold_font = Font(bold=True) thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin')) # 1행 스타일 for cell in worksheet[1]: if cell.value == '▼작성': cell.fill = bold_yellow_fill cell.font = bold_font # 2행 스타일 for cell in worksheet[2]: cell.alignment = center_align cell.border = thin_border # 데이터 행 스타일 left_align_cols = ['상세분류ID', '상세분류명', '규격'] col_map = {col: i + 1 for i, col in enumerate(columns_for_excel)} for row_num in range(3, worksheet.max_row + 1): for col_name in columns_for_excel: cell = worksheet.cell(row=row_num, column=col_map[col_name]) cell.alignment = left_align if col_name in left_align_cols else center_align cell.border = thin_border # 열 너비 자동 조정 for column in worksheet.columns: max_length = 0 column_letter = get_column_letter(column[0].column) for cell in column: if cell.value: max_length = max(max_length, len(str(cell.value))) worksheet.column_dimensions[column_letter].width = max_length + (20 if column_letter == get_column_letter(col_map['규격']) else 5) print(f"✅ '{file_name}' 파일이 '{output_dir}' 폴더에 생성되었습니다.") # 이메일 정보 partner_email = group_df['이메일주소'].iloc[0] partner_name = group_df['협력사명'].iloc[0] if pd.isna(partner_email): print(f"❌ {partner_name}의 이메일 주소가 없어 이메일 발송을 건너뜁니다.") continue print(f"📧 '{partner_name}' ({partner_email})님에게 이메일 발송 중...") msg = EmailMessage() msg['From'] = formataddr((str(Header('보내는 사람 이름', 'utf-8')), SENDER_EMAIL)) msg['To'] = partner_email # 이 부분만 이전 코드로 되돌려 일반 문자열을 할당합니다. msg['Subject'] = f'[{partner_name}] 월간 비용 견적 요청서입니다.' body = f"""안녕하세요, {partner_name} 담당자님. 월간 비용 견적 요청서(첨부파일)를 보내드립니다. 확인 후 회신 부탁드립니다. 감사합니다. -- 보내는 사람 이름 """ msg.set_content(body) try: with open(output_path, 'rb') as f: # `filename` 인수를 일반 문자열로 전달 msg.add_attachment(f.read(), maintype='application', subtype='octet-stream', filename=file_name) except Exception as e: print(f"❌ 파일 첨부 중 오류 발생: {e}") continue try: context = ssl.create_default_context() with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: server.starttls(context=context) server.login(SENDER_EMAIL, SENDER_PASSWORD) server.send_message(msg) print(f"✅ 이메일 발송 성공: {partner_name}") except Exception as e: print(f"❌ 이메일 발송 실패: {partner_name} - {e}")