**결론**: 한국식 날짜 형식("2025. 9. 23. 오전 9:44")을 파싱하고 매 시간별로 분석하는 코드입니다.

**구현**:
```python
import re
from datetime import datetime
from collections import defaultdict, Counter

def analyze_chat_statistics(file_path):
    """
    텍스트 파일에서 시간과 대화 내용을 분석하여 통계 생성
    
    지원 형식: "2025. 9. 23. 오전 9:44"
    """
    
    # 통계 저장용 딕셔너리
    stats = {
        'daily_count': defaultdict(int),      # 일자별 대화 수
        'hourly_count': Counter(),            # 시간대별 대화 수 (0-23시)
        'weekday_count': Counter(),           # 요일별 대화 수
        'total_messages': 0,
        'date_range': {'start': None, 'end': None}
    }
    
    # 정규표현식: "2025. 9. 23. 오전 9:44" 형식
    pattern = r'(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\.\s*(오전|오후)\s*(\d{1,2}):(\d{2})'
    
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line_num, line in enumerate(f, 1):
                line = line.strip()
                if not line:
                    continue
                
                match = re.search(pattern, line)
                if match:
                    try:
                        year = int(match.group(1))
                        month = int(match.group(2))
                        day = int(match.group(3))
                        ampm = match.group(4)
                        hour = int(match.group(5))
                        minute = int(match.group(6))
                        
                        # 오전/오후를 24시간 형식으로 변환
                        if ampm == '오후' and hour != 12:
                            hour += 12
                        elif ampm == '오전' and hour == 12:
                            hour = 0
                        
                        # 날짜 객체 생성
                        date_obj = datetime(year, month, day, hour, minute)
                        date_key = date_obj.strftime('%Y-%m-%d')
                        weekday = ['월', '화', '수', '목', '금', '토', '일'][date_obj.weekday()]
                        
                        # 통계 업데이트
                        stats['total_messages'] += 1
                        stats['daily_count'][date_key] += 1
                        stats['weekday_count'][weekday] += 1
                        stats['hourly_count'][hour] += 1
                        
                        # 날짜 범위 업데이트
                        if stats['date_range']['start'] is None or date_obj < stats['date_range']['start']:
                            stats['date_range']['start'] = date_obj
                        if stats['date_range']['end'] is None or date_obj > stats['date_range']['end']:
                            stats['date_range']['end'] = date_obj
                        
                    except (ValueError, IndexError) as e:
                        print(f"⚠️  {line_num}번째 줄 파싱 오류: {e}")
                        print(f"    내용: {line[:100]}")
                        continue
                        
    except FileNotFoundError:
        print(f"❌ 파일을 찾을 수 없습니다: {file_path}")
        return None
    except Exception as e:
        print(f"❌ 파일 읽기 오류: {e}")
        return None
    
    return stats


def print_statistics(stats):
    """통계 결과를 보기 좋게 출력"""
    
    if stats is None or stats['total_messages'] == 0:
        print("분석할 데이터가 없습니다.")
        return
    
    print("\n" + "="*60)
    print("📊 대화 통계 분석 결과")
    print("="*60)
    
    # 전체 통계
    print(f"\n📝 전체 메시지 수: {stats['total_messages']:,}개")
    
    if stats['date_range']['start']:
        print(f"📅 기간: {stats['date_range']['start'].strftime('%Y-%m-%d')} ~ "
              f"{stats['date_range']['end'].strftime('%Y-%m-%d')}")
        days_diff = (stats['date_range']['end'] - stats['date_range']['start']).days + 1
        print(f"   (총 {days_diff}일간)")
        print(f"📈 일평균: {stats['total_messages'] / days_diff:.1f}개")
    
    # 일자별 통계 (상위 10개)
    if stats['daily_count']:
        print("\n" + "-"*60)
        print("📆 일자별 대화량 (상위 10일)")
        print("-"*60)
        sorted_daily = sorted(stats['daily_count'].items(), key=lambda x: x[1], reverse=True)[:10]
        max_daily = max(count for _, count in sorted_daily)
        for date, count in sorted_daily:
            bar = '█' * (count * 40 // max_daily)
            print(f"{date}: {count:4d}개 {bar}")
    
    # 요일별 통계
    if stats['weekday_count']:
        print("\n" + "-"*60)
        print("📅 요일별 대화량")
        print("-"*60)
        weekdays = ['월', '화', '수', '목', '금', '토', '일']
        max_count = max(stats['weekday_count'].values()) if stats['weekday_count'] else 1
        for day in weekdays:
            count = stats['weekday_count'][day]
            bar = '█' * (count * 40 // max_count) if count > 0 else ''
            percentage = (count / stats['total_messages'] * 100) if stats['total_messages'] > 0 else 0
            print(f"{day}요일: {count:4d}개 ({percentage:5.1f}%) {bar}")
    
    # 시간대별 통계 (매 시간별)
    if stats['hourly_count']:
        print("\n" + "-"*60)
        print("🕐 시간별 대화량 (매 시간)")
        print("-"*60)
        max_count = max(stats['hourly_count'].values())
        for hour in range(24):
            count = stats['hourly_count'][hour]
            bar = '█' * (count * 40 // max_count) if count > 0 else ''
            percentage = (count / stats['total_messages'] * 100) if stats['total_messages'] > 0 else 0
            
            # 오전/오후 표시
            if hour == 0:
                time_str = "오전 12시"
            elif hour < 12:
                time_str = f"오전 {hour:2d}시"
            elif hour == 12:
                time_str = "오후 12시"
            else:
                time_str = f"오후 {hour-12:2d}시"
            
            print(f"{time_str}: {count:4d}개 ({percentage:5.1f}%) {bar}")
    
    # 시간대 구간별 요약
    if stats['hourly_count']:
        print("\n" + "-"*60)
        print("⏰ 시간대 구간별 요약")
        print("-"*60)
        time_periods = {
            '새벽 (00-06시)': sum(stats['hourly_count'][h] for h in range(0, 6)),
            '오전 (06-12시)': sum(stats['hourly_count'][h] for h in range(6, 12)),
            '오후 (12-18시)': sum(stats['hourly_count'][h] for h in range(12, 18)),
            '저녁 (18-24시)': sum(stats['hourly_count'][h] for h in range(18, 24))
        }
        for period, count in time_periods.items():
            percentage = (count / stats['total_messages'] * 100) if stats['total_messages'] > 0 else 0
            print(f"{period}: {count:5d}개 ({percentage:5.1f}%)")
    
    # 가장 활발한 시간대
    if stats['hourly_count']:
        most_active_hour = max(stats['hourly_count'].items(), key=lambda x: x[1])
        if most_active_hour[0] < 12:
            time_display = f"오전 {most_active_hour[0] if most_active_hour[0] != 0 else 12}시"
        else:
            time_display = f"오후 {most_active_hour[0]-12 if most_active_hour[0] != 12 else 12}시"
        
        print(f"\n🔥 가장 활발한 시간: {time_display} ({most_active_hour[1]}개)")
    
    # 가장 활발한 요일
    if stats['weekday_count']:
        most_active_day = max(stats['weekday_count'].items(), key=lambda x: x[1])
        print(f"🔥 가장 활발한 요일: {most_active_day[0]}요일 ({most_active_day[1]}개)")
    
    print("\n" + "="*60)


# 실행 예시
if __name__ == "__main__":
    # 파일 경로 지정
    file_path = "chat_log.txt"  # 여기에 실제 파일명을 입력하세요
    
    print("🔍 파일 분석 중...")
    stats = analyze_chat_statistics(file_path)
    print_statistics(stats)
```

**주요 변경사항**:
1. ✅ **날짜 형식**: "2025. 9. 23. 오전 9:44" 파싱
2. ✅ **오전/오후 변환**: 자동으로 24시간 형식으로 변환
3. ✅ **매 시간별 분석**: 0시~23시까지 각 시간별 통계
4. ✅ **시간 표시**: 출력 시 "오전 9시", "오후 3시" 형식으로 표시
5. ✅ **퍼센트 추가**: 각 시간대와 요일별 비율 표시
6. ✅ **최다 시간/요일**: 가장 활발한 시간대와 요일 표시

**사용 방법**:
```bash
python chat_stats.py
```

파일 경로만 수정하면 바로 실행됩니다! 📊

# 프로젝트 루트 디렉토리 생성
mkdir -p ~/projects/mcp
cd ~/projects/mcp

# 가상환경 생성
python -m venv .venv

# 활성화
source .venv/bin/activate

# pip 최신 버전으로
pip install --upgrade pip

# uv 설치 (MCP 패키지 매니저)
pip install uv

# 또는 일반 pip로
pip install "mcp[cli]"

mcp

# 현재 패키지 목록 저장
pip freeze > requirements.txt

# 확인
cat requirements.txt

# 프롬프트에서 (venv) 사라지면 성공
deactivate


# 한 줄에 모두 입력
cd ~/projects/mcp && source .venv/bin/activate

# && 의미: 앞 명령어가 성공하면 뒤 명령어 실행
# ========== Step 1: Xcode Command Line Tools ==========
xcode-select --install
# 팝업에서 "설치" 클릭, 완료 대기 (5-10분)

# ========== Step 2: Homebrew 설치 ==========
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Enter 키, 비밀번호 입력, 완료 대기 (5-10분)

# ========== Step 3: 환경변수 설정 ==========
# M1/M2/M3 Mac
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

# Intel Mac
echo 'eval "$(/usr/local/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/usr/local/bin/brew shellenv)"

# ========== Step 4: 확인 ==========
brew --version
brew doctor  # 문제 있으면 알려줌

# ========== Step 5: pyenv 설치 ==========
brew install pyenv

# ========== Step 6: pyenv 환경변수 ==========
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init -)"' >> ~/.zshrc
source ~/.zshrc

# ========== Step 7: Python 설치 ==========
pyenv install 3.11.9
pyenv global 3.11.9
python --version

# ========== 완료! ==========

# Homebrew 설치 후 바로 설치하면 좋은 것들

# 1. pyenv (Python 버전 관리)
brew install pyenv

# 2. Git (버전 관리)
brew install git

# 3. iTerm2 (더 나은 터미널)
brew install --cask iterm2

# 4. VS Code (코드 에디터)
brew install --cask visual-studio-code

# 5. 기본 도구들
brew install wget curl tree

brew install font-meslo-lg-nerd-font
# Oh My Zsh 설치
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

# Powerlevel10k 설치
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k

# ~/.zshrc 수정
nano ~/.zshrc

# 아래 줄 찾아서:
ZSH_THEME="robbyrussell"

# 이렇게 변경:
ZSH_THEME="powerlevel10k/powerlevel10k"

# 저장: Ctrl+O → Enter → Ctrl+X

# 자동완성 제안 (회색 글씨로 명령어 제안)
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

# 문법 하이라이팅 (명령어 색상)
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

# 확인
ls ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/
# zsh-autosuggestions, zsh-syntax-highlighting 보이면 성공

nano ~/.zshrc
plugins=(
  git
  python
  pyenv
  brew
  zsh-autosuggestions
  zsh-syntax-highlighting
)

source ~/.zshrc


# iTerm2 실행
open -a iTerm

# Dracula 테마 다운로드
cd ~/Downloads
curl -o Dracula.itermcolors https://raw.githubusercontent.com/dracula/iterm/master/Dracula.itermcolors

iTerm2 → Preferences → Profiles → Colors
Color Presets → Import
Dracula.itermcolors 선택
Dracula 선택


기본 프로필로 설정:

Profiles → Other Actions → Set as Default

시간대 09:00 서울로 설정

서버 매니저 > 로컬 서버 >  IE Enhanced Security configuration (OFF) 

IE 브라우저 재시작 

ChromeSetup.exe
10.37MB

 

BANDIZIP6-SETUP (1).EXE
6.26MB
sp timesync.zip
0.05MB

time.google.com

 

https://www.python.org/ftp/python/3.9.7/python-3.9.7-amd64.exe

 

requirements (1).txt
0.00MB

편하게 C드라이브 상단에 저장할것

pip install --upgrade pip

# → 패키지 설치관리 도구(pip)를 최신버전으로 만든다. #오류난다면 아래 명령어로 실행

python -m pip install --upgrade pip

# (4) 필요한 패키지 설치

pip install -r requirements.txt

# → 현재 폴더의 requirements.txt 파일에 적힌 모든 패키지들을 한 번에 설치한다.

 

 

(venv)가 앞에 붙어있으면됨   아래는 가상환경할때

# (1) 가상환경 생성  
python -m venv venv
# → "venv"라는 이름으로 파이썬 가상환경 폴더를 만든다.
#   이 폴더 안에만 독립적으로 패키지가 설치된다.

# (2) 가상환경 활성화 (실행)
venv\Scripts\activate
# → "venv" 가상환경 안으로 진입.  
#   (명령 프롬프트 앞에 (venv) 표시가 붙는다)

# (3) pip 업그레이드
pip install --upgrade pip
# → 패키지 설치관리 도구(pip)를 최신버전으로 만든다.
#오류난다면 아래 명령어로 실행
python -m pip install --upgrade pip

# (4) 필요한 패키지 설치
pip install -r requirements.txt
# → 현재 폴더의 requirements.txt 파일에 적힌 모든 패키지들을 한 번에 설치한다.


-----------------------------------------------------------------------
(1) 명령 프롬프트(CMD) 창을 새로 열었다면
cd 프로젝트폴더경로
# → 다시 작업할 프로젝트 폴더로 이동

venv\Scripts\activate
# → 위와 같이 가상환경을 재활성화(진입)


(2) 가상환경에서 빠져나오는 방법 (비활성화)
deactivate
# → 가상환경에서 나온다. (원래 시스템 파이썬 상태로 돌아감)

import os
import glob
import re
from pathlib import Path
from typing import Iterator, Optional, Union

def filter_lines_memory_efficient(input_file: str, output_file: str, keyword: str, 
                                 case_sensitive: bool = True, encoding: str = 'utf-8') -> int:
    """
    메모리 효율적인 줄 단위 필터링 - 대용량 파일 지원
    
    Args:
        input_file: 입력 파일 경로
        output_file: 출력 파일 경로
        keyword: 검색할 키워드
        case_sensitive: 대소문자 구분 여부
        encoding: 파일 인코딩
    
    Returns:
        필터링된 줄 수
    """
    try:
        line_count = 0
        search_keyword = keyword if case_sensitive else keyword.lower()
        
        # 줄 단위로 스트리밍 처리 - 메모리 효율적
        with open(input_file, 'r', encoding=encoding, buffering=8192) as infile, \
             open(output_file, 'w', encoding=encoding, buffering=8192) as outfile:
            
            for line in infile:  # 파이썬의 내장 이터레이터 사용
                check_line = line if case_sensitive else line.lower()
                if search_keyword in check_line:
                    outfile.write(line)
                    line_count += 1
        
        print(f"✅ {Path(input_file).name}: {line_count:,}줄 필터링 완료")
        return line_count
        
    except FileNotFoundError:
        print(f"❌ 파일을 찾을 수 없습니다: {input_file}")
        return 0
    except PermissionError:
        print(f"❌ 파일 접근 권한이 없습니다: {input_file}")
        return 0
    except UnicodeDecodeError as e:
        print(f"❌ 인코딩 오류 {input_file}: {e}")
        print(f"💡 다른 인코딩 시도: cp949, euc-kr")
        return 0
    except Exception as e:
        print(f"❌ 예상치 못한 오류 {input_file}: {e}")
        return 0

def filter_with_regex_efficient(input_file: str, output_file: str, pattern: str,
                               flags: int = 0, encoding: str = 'utf-8') -> int:
    """
    정규표현식 기반 메모리 효율적 필터링
    
    Args:
        input_file: 입력 파일 경로
        output_file: 출력 파일 경로  
        pattern: 정규표현식 패턴
        flags: re 플래그
        encoding: 파일 인코딩
    """
    try:
        compiled_pattern = re.compile(pattern, flags)
        line_count = 0
        
        with open(input_file, 'r', encoding=encoding, buffering=8192) as infile, \
             open(output_file, 'w', encoding=encoding, buffering=8192) as outfile:
            
            for line in infile:
                if compiled_pattern.search(line):
                    outfile.write(line)
                    line_count += 1
                    
        print(f"✅ 정규표현식 필터링: {line_count:,}줄 처리됨")
        return line_count
        
    except re.error as e:
        print(f"❌ 정규표현식 오류: {e}")
        return 0
    except Exception as e:
        print(f"❌ 필터링 오류: {e}")
        return 0

def detect_encoding(file_path: str) -> str:
    """
    파일 인코딩 자동 감지
    """
    encodings_to_try = ['utf-8', 'cp949', 'euc-kr', 'utf-16', 'latin1']
    
    for encoding in encodings_to_try:
        try:
            with open(file_path, 'r', encoding=encoding) as f:
                f.read(1024)  # 첫 1KB만 테스트
                return encoding
        except (UnicodeDecodeError, UnicodeError):
            continue
    
    return 'utf-8'  # 기본값

def batch_filter_files_improved(directory: str, keyword: str, 
                               pattern: str = "*.txt", case_sensitive: bool = True,
                               backup: bool = True) -> dict:
    """
    개선된 일괄 파일 처리 - 인코딩 자동감지, 백업 옵션
    
    Args:
        directory: 처리할 디렉토리
        keyword: 검색 키워드
        pattern: 파일 패턴
        case_sensitive: 대소문자 구분
        backup: 원본 백업 여부
    
    Returns:
        처리 결과 딕셔너리
    """
    files = list(Path(directory).glob(pattern))
    
    if not files:
        print(f"❌ {directory}에서 {pattern} 패턴 파일을 찾을 수 없습니다.")
        return {"processed": 0, "total": 0, "lines": 0}
    
    print(f"🔍 {len(files)}개 파일에서 '{keyword}' 검색 시작...")
    
    processed_files = 0
    total_lines = 0
    failed_files = []
    
    for file_path in files:
        # 인코딩 자동 감지
        encoding = detect_encoding(str(file_path))
        print(f"📄 {file_path.name} (인코딩: {encoding})")
        
        # 백업 생성
        if backup:
            backup_path = file_path.with_suffix('.bak' + file_path.suffix)
            if not backup_path.exists():
                file_path.replace(backup_path)
                file_path = backup_path
        
        # 출력 파일명
        output_path = file_path.parent / f"{file_path.stem}_filtered{file_path.suffix}"
        
        lines_found = filter_lines_memory_efficient(
            str(file_path), str(output_path), keyword, 
            case_sensitive=case_sensitive, encoding=encoding
        )
        
        if lines_found > 0:
            processed_files += 1
            total_lines += lines_found
        elif lines_found == -1:  # 오류 발생
            failed_files.append(str(file_path))
    
    # 결과 요약
    result = {
        "processed": processed_files,
        "total": len(files),
        "lines": total_lines,
        "failed": failed_files
    }
    
    print(f"\n📊 처리 완료:")
    print(f"   ✅ 성공: {processed_files}/{len(files)}개 파일")
    print(f"   📝 총 필터링된 줄: {total_lines:,}")
    if failed_files:
        print(f"   ❌ 실패한 파일: {len(failed_files)}개")
        for failed in failed_files[:3]:  # 최대 3개만 표시
            print(f"      - {Path(failed).name}")
    
    return result

def get_file_info(file_path: str) -> dict:
    """
    파일 정보 확인 - 크기, 줄 수 등
    """
    try:
        path = Path(file_path)
        size = path.stat().st_size
        
        # 줄 수 계산 (메모리 효율적)
        line_count = 0
        encoding = detect_encoding(file_path)
        with open(file_path, 'r', encoding=encoding) as f:
            for _ in f:
                line_count += 1
        
        return {
            "size_mb": round(size / (1024 * 1024), 2),
            "lines": line_count,
            "encoding": encoding
        }
    except Exception as e:
        return {"error": str(e)}

def interactive_mode():
    """
    대화형 모드 - 사용자 친화적 인터페이스
    """
    print("=" * 50)
    print("🔍 텍스트 파일 필터링 도구 v2.0")
    print("=" * 50)
    
    while True:
        print("\n처리 방식 선택:")
        print("1. 단일 파일 처리")
        print("2. 여러 파일 일괄 처리")
        print("3. 정규표현식 필터링")
        print("4. 파일 정보 확인")
        print("5. 종료")
        
        choice = input("\n선택 (1-5): ").strip()
        
        if choice == "1":
            # 단일 파일 처리
            input_file = input("입력 파일 경로: ").strip()
            if not Path(input_file).exists():
                print("❌ 파일이 존재하지 않습니다.")
                continue
                
            keyword = input("검색할 키워드 (기본: 삐삐): ").strip() or "삐삐"
            case_sensitive = input("대소문자 구분? (y/N): ").strip().lower() == 'y'
            
            # 출력 파일명 자동 생성
            input_path = Path(input_file)
            output_file = str(input_path.parent / f"{input_path.stem}_filtered{input_path.suffix}")
            
            # 파일 정보 표시
            info = get_file_info(input_file)
            if "error" not in info:
                print(f"📊 파일 정보: {info['size_mb']}MB, {info['lines']:,}줄, {info['encoding']}")
            
            filter_lines_memory_efficient(input_file, output_file, keyword, case_sensitive)
            
        elif choice == "2":
            # 여러 파일 일괄 처리
            directory = input("디렉토리 경로 (기본: 현재 폴더): ").strip() or "."
            pattern = input("파일 패턴 (기본: *.txt): ").strip() or "*.txt"
            keyword = input("검색할 키워드 (기본: 삐삐): ").strip() or "삐삐"
            case_sensitive = input("대소문자 구분? (y/N): ").strip().lower() == 'y'
            backup = input("원본 백업? (Y/n): ").strip().lower() != 'n'
            
            batch_filter_files_improved(directory, keyword, pattern, case_sensitive, backup)
            
        elif choice == "3":
            # 정규표현식 필터링
            input_file = input("입력 파일 경로: ").strip()
            if not Path(input_file).exists():
                print("❌ 파일이 존재하지 않습니다.")
                continue
                
            pattern = input("정규표현식 패턴: ").strip()
            ignore_case = input("대소문자 무시? (y/N): ").strip().lower() == 'y'
            flags = re.IGNORECASE if ignore_case else 0
            
            input_path = Path(input_file)
            output_file = str(input_path.parent / f"{input_path.stem}_regex_filtered{input_path.suffix}")
            
            filter_with_regex_efficient(input_file, output_file, pattern, flags)
            
        elif choice == "4":
            # 파일 정보 확인
            file_path = input("파일 경로: ").strip()
            if Path(file_path).exists():
                info = get_file_info(file_path)
                if "error" in info:
                    print(f"❌ 오류: {info['error']}")
                else:
                    print(f"📊 파일 정보:")
                    print(f"   크기: {info['size_mb']}MB")
                    print(f"   줄 수: {info['lines']:,}")
                    print(f"   인코딩: {info['encoding']}")
            else:
                print("❌ 파일이 존재하지 않습니다.")
                
        elif choice == "5":
            print("👋 프로그램을 종료합니다.")
            break
        else:
            print("❌ 잘못된 선택입니다.")

# 성능 테스트 함수
def performance_test(file_path: str, keyword: str):
    """
    성능 테스트 - 처리 시간 및 메모리 사용량 측정
    """
    import time
    import psutil
    import os
    
    print(f"🚀 성능 테스트 시작: {Path(file_path).name}")
    
    # 메모리 사용량 측정
    process = psutil.Process(os.getpid())
    memory_before = process.memory_info().rss / 1024 / 1024  # MB
    
    start_time = time.time()
    
    # 임시 출력 파일
    temp_output = file_path + ".temp_filtered"
    
    lines_found = filter_lines_memory_efficient(file_path, temp_output, keyword)
    
    end_time = time.time()
    memory_after = process.memory_info().rss / 1024 / 1024  # MB
    
    # 임시 파일 삭제
    if Path(temp_output).exists():
        Path(temp_output).unlink()
    
    print(f"📊 성능 결과:")
    print(f"   처리 시간: {end_time - start_time:.2f}초")
    print(f"   메모리 사용량: {memory_before:.1f} → {memory_after:.1f} MB")
    print(f"   메모리 증가: {memory_after - memory_before:.1f} MB")
    print(f"   처리된 줄: {lines_found:,}")

if __name__ == "__main__":
    interactive_mode()

**결론**: "타입에러 낫 어 펑션" 오류는 JavaScript 문법 문제입니다. Frida JavaScript 환경에 맞게 수정한 스크립트를 제공합니다.

## 🛠️ 수정된 안전한 스크립트

```python
import frida
import time
import sys
import os

def fixed_memory_analysis():
    mail_exe_path = "mail.exe"
    
    # 수정된 JavaScript 코드 (안전한 버전)
    js_code = """
    console.log("=== MAIL.EXE 메모리 분석 시작 ===");
    
    function safeMemoryAnalysis() {
        try {
            // 1. 프로세스 기본 정보
            console.log("\\n[1] 프로세스 정보:");
            console.log("PID: " + Process.id);
            console.log("아키텍처: " + Process.arch);
            console.log("플랫폼: " + Process.platform);
            
            // 2. 모듈 찾기
            console.log("\\n[2] 모듈 검색:");
            var modules = Process.enumerateModules();
            console.log("총 모듈 개수: " + modules.length);
            
            var targetModule = null;
            
            // 첫 10개 모듈 출력
            for (var i = 0; i < Math.min(modules.length, 10); i++) {
                var mod = modules[i];
                console.log("모듈[" + i + "]: " + mod.name + " -> " + mod.base);
                
                if (i === 0) {
                    targetModule = mod;  // 첫 번째 모듈을 타겟으로
                }
            }
            
            if (!targetModule) {
                console.log("ERROR: 사용할 모듈이 없습니다");
                return;
            }
            
            console.log("\\n[3] 타겟 모듈: " + targetModule.name);
            console.log("베이스 주소: " + targetModule.base);
            console.log("모듈 크기: " + targetModule.size);
            
            // 3. 주소 계산
            var baseAddr = targetModule.base;
            var offset = 0x4050cf;
            var targetAddr = baseAddr.add(offset);
            
            console.log("\\n[4] 주소 계산:");
            console.log("베이스: " + baseAddr);
            console.log("오프셋: 0x" + offset.toString(16));
            console.log("타겟: " + targetAddr);
            
            // 4. 메모리 접근 테스트 (안전한 방법)
            console.log("\\n[5] 메모리 접근 테스트:");
            
            try {
                // 한 바이트씩 안전하게 읽기
                var firstByte = Memory.readU8(targetAddr);
                console.log("첫 번째 바이트: 0x" + firstByte.toString(16));
                
                if (firstByte === 0x68) {
                    console.log("SUCCESS: PUSH 명령어 발견!");
                    
                    // PUSH 오퍼랜드 읽기
                    var operand = Memory.readU32(targetAddr.add(1));
                    console.log("PUSH 오퍼랜드: 0x" + operand.toString(16));
                    
                    // 문자열 주소로 변환
                    var stringAddr = ptr(operand);
                    console.log("문자열 주소: " + stringAddr);
                    
                    // 문자열 읽기 시도
                    try {
                        var originalString = Memory.readUtf8String(stringAddr);
                        console.log("원본 문자열: '" + originalString + "'");
                        
                        // 패치 시도
                        performPatch(targetAddr, stringAddr, originalString);
                        
                    } catch (stringError) {
                        console.log("문자열 읽기 실패: " + stringError.message);
                    }
                    
                } else {
                    console.log("WARNING: PUSH 명령어가 아닙니다 (0x" + firstByte.toString(16) + ")");
                    
                    // 주변 바이트들 확인
                    console.log("주변 바이트들:");
                    for (var i = 0; i < 8; i++) {
                        try {
                            var b = Memory.readU8(targetAddr.add(i));
                            console.log("  [+" + i + "] 0x" + b.toString(16));
                        } catch (e) {
                            console.log("  [+" + i + "] 읽기 실패");
                        }
                    }
                }
                
            } catch (accessError) {
                console.log("메모리 접근 실패: " + accessError.message);
                
                // 대안: 문자열 패턴 검색
                searchStringPattern();
            }
            
        } catch (mainError) {
            console.log("메인 분석 오류: " + mainError.message);
            console.log("스택: " + mainError.stack);
        }
    }
    
    function performPatch(pushAddr, stringAddr, originalString) {
        console.log("\\n[6] 패치 수행:");
        
        try {
            // 새 문자열 할당
            var newString = "test";
            var newStringAddr = Memory.alloc(newString.length + 1);
            Memory.writeUtf8String(newStringAddr, newString);
            
            console.log("새 문자열 할당: " + newStringAddr);
            
            // 메모리 보호 해제
            Memory.protect(pushAddr, 8, 'rwx');
            console.log("메모리 보호 해제 완료");
            
            // PUSH 오퍼랜드 변경
            Memory.writeU32(pushAddr.add(1), newStringAddr.toInt32());
            console.log("PUSH 오퍼랜드 변경 완료");
            
            // 검증
            var newOperand = Memory.readU32(pushAddr.add(1));
            console.log("새 오퍼랜드: 0x" + newOperand.toString(16));
            
            var verifyString = Memory.readUtf8String(newStringAddr);
            console.log("새 문자열 검증: '" + verifyString + "'");
            
            console.log("SUCCESS: 패치 완료!");
            
        } catch (patchError) {
            console.log("패치 실패: " + patchError.message);
        }
    }
    
    function searchStringPattern() {
        console.log("\\n[7] 문자열 패턴 검색:");
        
        try {
            var targetString = "Not available in proxy mode.";
            var ranges = Process.enumerateRanges('r--');
            
            console.log("검색할 메모리 범위: " + ranges.length + "개");
            
            for (var i = 0; i < Math.min(ranges.length, 50); i++) {
                try {
                    var results = Memory.scanSync(ranges[i].base, ranges[i].size, targetString);
                    
                    if (results.length > 0) {
                        console.log("문자열 발견! 주소: " + results[0].address);
                        
                        // 직접 문자열 변경 시도
                        Memory.protect(results[0].address, targetString.length, 'rwx');
                        Memory.writeUtf8String(results[0].address, "test");
                        
                        // 나머지 공간 NULL로 채우기
                        for (var j = 4; j < targetString.length; j++) {
                            Memory.writeU8(results[0].address.add(j), 0);
                        }
                        
                        console.log("SUCCESS: 문자열 직접 변경 완료!");
                        return;
                    }
                    
                } catch (scanError) {
                    // 스캔 오류는 무시하고 계속
                }
            }
            
            console.log("문자열 패턴을 찾을 수 없습니다");
            
        } catch (searchError) {
            console.log("검색 오류: " + searchError.message);
        }
    }
    
    // 1초 후 실행
    setTimeout(function() {
        safeMemoryAnalysis();
    }, 1000);
    """
    
    try:
        print("[*] mail.exe 실행 중...")
        
        # 파일 확인
        if not os.path.exists(mail_exe_path):
            print(f"[-] {mail_exe_path} 파일이 없습니다.")
            return
        
        # 프로세스 스폰
        pid = frida.spawn([mail_exe_path])
        print(f"[+] 프로세스 생성 (PID: {pid})")
        
        # 세션 연결
        session = frida.attach(pid)
        print("[+] Frida 연결 완료")
        
        # 스크립트 생성
        script = session.create_script(js_code)
        
        # 메시지 처리
        def on_message(message, data):
            if message['type'] == 'send':
                print(f"[JS] {message['payload']}")
            elif message['type'] == 'error':
                print(f"[ERROR] {message['description']}")
                if 'stack' in message:
                    print(f"[STACK] {message['stack']}")
        
        script.on('message', on_message)
        script.load()
        
        # 프로세스 재개
        frida.resume(pid)
        print("[+] 프로세스 재개됨")
        
        print("\n" + "="*40)
        print("분석 로그:")
        print("="*40)
        
        # 분석 대기
        time.sleep(4)
        
        print("\n분석 완료!")
        input("엔터를 눌러 종료...")
        
    except Exception as e:
        print(f"[-] 오류: {e}")
        import traceback
        traceback.print_exc()

if __name__ == "__main__":
    fixed_memory_analysis()
```

## 🔧 최소한의 테스트 스크립트

```python
import frida
import time

def minimal_test():
    """최소한의 기능으로 테스트"""
    
    js_code = """
    console.log("=== 최소 테스트 시작 ===");
    
    setTimeout(function() {
        try {
            console.log("PID: " + Process.id);
            
            var modules = Process.enumerateModules();
            console.log("모듈 수: " + modules.length);
            
            if (modules.length > 0) {
                var main = modules[0];
                console.log("메인 모듈: " + main.name);
                console.log("베이스: " + main.base);
                
                var target = main.base.add(0x4050cf);
                console.log("타겟: " + target);
                
                var byte = Memory.readU8(target);
                console.log("바이트: 0x" + byte.toString(16));
                
                if (byte === 0x68) {
                    console.log("PUSH 발견!");
                    var addr = Memory.readU32(target.add(1));
                    console.log("주소: 0x" + addr.toString(16));
                    
                    var str = Memory.readUtf8String(ptr(addr));
                    console.log("문자열: " + str);
                } else {
                    console.log("PUSH 아님");
                }
                
            }
        } catch (e) {
            console.log("오류: " + e.message);
        }
    }, 500);
    """
    
    try:
        pid = frida.spawn(["mail.exe"])
        session = frida.attach(pid)
        script = session.create_script(js_code)
        
        def on_message(message, data):
            print(message['payload'])
        
        script.on('message', on_message)
        script.load()
        frida.resume(pid)
        
        time.sleep(2)
        
    except Exception as e:
        print(f"오류: {e}")

if __name__ == "__main__":
    minimal_test()
```

## ⚠️ 문제 해결 팁

### 1. JavaScript 문법 확인
- `padStart()` 같은 ES6 함수 사용 금지
- 문자열 연결은 `+` 연산자만 사용
- `try-catch` 블록으로 모든 메모리 접근 보호

### 2. Frida 버전 확인
```bash
frida --version
pip install --upgrade frida-tools
```

### 3. 권한 문제
```bash
# 관리자 권한으로 실행
# 또는 UAC 설정 확인
```

최소 테스트 스크립트부터 실행해서 단계별로 문제를 확인해보세요. 어떤 오류 메시지가 나오는지 알려주시면 더 구체적으로 도와드리겠습니다.

1. 설치 및 설정

의존성 설치:

brew install readline qt5 pkgconfig p7zip libusb arm-none-eabi-gcc

소스코드 다운로드 및 컴파일:

git clone https://github.com/RfidResearchGroup/proxmark3.git
cd proxmark3
make clean && make all

연결 확인:

system_profiler SPUSBDataType | grep -i proxmark
ls /dev/tty.usbmodem*

2. Proxmark3 실행 및 하드웨어 확인

클라이언트 실행:

./pm3 -p /dev/tty.usbmodemiceman1

하드웨어 상태 확인:

hw status
hw version

3. 카드 분석

주파수별 스캔:

lf search    # 125kHz 카드 확인 (EM4x70 감지됨)
hf search    # 13.56MHz 카드 확인 (Mifare Classic 1K 발견)

Mifare 상세 정보:

hf mf info   # UID: 47XXXXXX, 모든 키가 FFFFFFFFFFFF 확인

4. 키 확인 및 덤프

키 체크 및 파일 생성:

hf mf chk --1k --dump
# 결과: /Users/user/hf-mf-47XXXXXX-key.bin 생성

카드 덤프:

hf mf dump --1k
# 결과: /Users/user/hf-mf-47XXXXXX-dump.bin 생성

5. CUID 카드에 복사

복사 실행:

hf mf restore --1k -f /Users/user/hf-mf-47XXXXXX-dump.bin -k /Users/user/hf-mf-47XXXXXX-key.bin

복사 검증:

hf mf dump --ns   # 복사된 카드 읽기 (파일 저장 안함)

핵심 발견사항

카드 특성:

  • 듀얼 주파수: EM4x70(125kHz) + Mifare Classic 1K(13.56MHz)
  • 실제 사용: Mifare Classic 부분 (UID: 47XXXXXX)
  • 보안 수준: 기본 키(FFFFFFFFFFFF) 사용으로 취약

 

 

공격 가능한 카드 (PRNG 사용)

MIFARE Classic 1K (MF1 S50)

  • SAK: 0x08
  • 공격: darkside, nested, hardnested 모두 가능
  • 성공률: 높음

MIFARE Classic 4K (MF1 S70)

  • SAK: 0x18
  • 공격: darkside, nested, hardnested 모두 가능
  • 성공률: 높음

저주파 카드들 (125kHz)

  • HID ProxCard
  • EM4100/EM4102
  • T5577 (재작성 가능)
  • Indala
  • 공격: 단순 복사/에뮬레이션
  • 성공률: 매우 높음

공격 어려운/불가능한 카드 (TRNG 사용)

MIFARE Plus

  • SAK: 0x08(Level 1), 0x20(Level 3)
  • 공격: 표준 방법 차단됨

SmartMX

  • SAK: 0x28
  • 공격: 모든 표준 공격 차단

MIFARE DESFire

  • SAK: 0x20
  • 공격: AES 암호화로 거의 불가능

iClass, Legic Prime

  • 고급 암호화
  • 공격: 매우 제한적

 

RFID/NFC 카드 공격 가능성 비교표

고주파 카드 (13.56MHz)

카드명 SAK 메모리 RNG 암호화 가능한 공격 성공률 복사용 카드

MIFARE Classic 1K 0x08 1KB PRNG Crypto1 darkside, nested, hardnested, autopwn 높음 CUID, UID
MIFARE Classic 4K 0x18 4KB PRNG Crypto1 darkside, nested, hardnested, autopwn 높음 CUID, UID
SmartMX + Classic 0x28 다양 TRNG AES/3DES 거의 불가능 매우 낮음 복사 불가
MIFARE Plus Lv1 0x08/0x18 2KB/4KB TRNG AES Classic 호환 모드에서만 제한적 낮음 제한적
MIFARE Plus Lv2 0x10/0x11 2KB/4KB TRNG AES 거의 불가능 매우 낮음 복사 불가
MIFARE Plus Lv3 0x20 2KB/4KB TRNG AES 거의 불가능 없음 복사 불가
MIFARE DESFire 0x20 2-8KB TRNG AES 거의 불가능 없음 복사 불가
CUID 0x08 1KB PRNG Crypto1 모든 공격 매우 높음 복사 대상
UID 0x08 1KB PRNG Crypto1 모든 공격 매우 높음 복사 대상

저주파 카드 (125kHz/134kHz)

카드명 주파수 메모리 보안 가능한 공격 성공률 복사용 카드

HID ProxCard 125kHz 64bit 없음 lf hid clone 매우 높음 T5577
EM4100/4102 125kHz 64bit 없음 lf em clone 매우 높음 T5577
EM4x70 125kHz 512bit 암호화 lf em 4x70 공격 낮음 T5577 (제한적)
Indala 125kHz 64bit 없음 lf indala clone 높음 T5577
T5577 125kHz 330bit 없음 직접 쓰기 매우 높음 복사 대상

공격 방법별 설명

고주파 공격 (13.56MHz)

공격 방법 필요 조건 시간 대상 카드 성공률 명령어 예시

autopwn 카드만 5-30분 PRNG 카드 높음 hf mf autopwn --1k
darkside 카드만 5-15분 PRNG 카드 높음 hf mf darkside
nested 1개 키 필요 1-5분 PRNG 카드 매우 높음 hf mf nested --1k --blk 0 -a -k FFFFFFFFFFFF
hardnested 1개 키 필요 10-60분 PRNG 카드 높음 hf mf hardnested --blk 0 -a -k FFFFFFFFFFFF --tblk 4 --ta
키 체크 카드만 30초-5분 모든 카드 카드별 차이 hf mf chk --1k
스니핑 공격 리더기 접근 수시간 모든 카드 중간 hf 14a sniff + mfkey64 분석

저주파 공격 (125kHz)

공격 방법 명령어 시간 성공률 설명

카드 식별 lf search 1-5초 매우 높음 카드 타입 자동 감지
EM410x 복사 lf em 410x clone --id [ID] 5-10초 매우 높음 EM4100/EM4102 → T5577
HID 복사 lf hid clone [TAG_ID] 5-10초 매우 높음 HID ProxCard → T5577
T5577 직접 쓰기 lf t55xx wr -b 0 -d [data] 10-30초 매우 높음 직접 데이터 쓰기
시뮬레이션 lf hid sim [TAG_ID] 즉시 매우 높음 카드 에뮬레이션

복사용 카드 정보

카드명 타입 가격 용도 특징

CUID 13.56MHz 1-3천원 Mifare Classic 복사 UID 변경 가능
UID 13.56MHz 1-2천원 Mifare Classic 복사 중국산 매직 카드
T5577 125kHz 1-2천원 LF 카드 복사 다양한 LF 카드 에뮬레이션

 

# QR 이미지에서 Google OTP 정보 추출하기

# 필요한 라이브러리 설치
# pip install opencv-python pyzbar pillow

import cv2
from pyzbar import pyzbar
import urllib.parse

def extract_otp_from_image(image_path):
    """
    이미지 파일에서 QR 코드를 읽어 OTP 정보 추출
    """
    try:
        print(f"이미지 분석 중: {image_path}")
        
        # 파일 존재 확인
        import os
        if not os.path.exists(image_path):
            print("❌ 파일이 존재하지 않습니다.")
            return
        
        # 여러 방법으로 이미지 읽기 시도
        image = None
        
        # 방법 1: OpenCV로 읽기
        try:
            image = cv2.imread(image_path)
        except:
            pass
            
        # 방법 2: PIL로 읽고 OpenCV 형식으로 변환
        if image is None:
            try:
                from PIL import Image
                import numpy as np
                pil_image = Image.open(image_path)
                # RGB to BGR 변환 (OpenCV 형식)
                image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
                print("✅ PIL로 이미지 읽기 성공")
            except Exception as e:
                print(f"PIL 읽기 실패: {e}")
        
        if image is None:
            print("❌ 이미지를 읽을 수 없습니다.")
            print("지원 형식: PNG, JPG, JPEG, BMP, GIF")
            return
        
        print(f"✅ 이미지 읽기 성공 (크기: {image.shape[1]}x{image.shape[0]})")
        
        # QR 코드 디코딩
        qr_codes = pyzbar.decode(image)
        
        if not qr_codes:
            print("❌ QR 코드를 찾을 수 없습니다.")
            print("💡 팁: QR 코드가 선명하고 전체가 보이는지 확인하세요.")
            return
        
        print(f"✅ {len(qr_codes)}개의 QR 코드 발견!")
        
        for i, qr_code in enumerate(qr_codes):
            print(f"\n--- QR 코드 #{i+1} ---")
            # QR 코드 데이터 추출
            qr_data = qr_code.data.decode('utf-8')
            
            # Google OTP 형식인지 확인
            if qr_data.startswith('otpauth://'):
                parse_otp_uri(qr_data)
            else:
                print("❌ Google OTP 형식이 아닙니다.")
                print(f"데이터: {qr_data[:100]}...")  # 처음 100자만 표시
                
    except Exception as e:
        print(f"❌ 오류 발생: {e}")
        print(f"오류 타입: {type(e).__name__}")
        import traceback
        traceback.print_exc()

def parse_otp_uri(uri):
    """
    OTP URI를 파싱하여 필요한 정보 추출
    """
    print(f"\n📱 Google OTP 정보 추출 완료!")
    print(f"원본 URI: {uri}")
    print(f"URI 길이: {len(uri)} 문자")
    
    # URI 파싱
    parsed = urllib.parse.urlparse(uri)
    params = urllib.parse.parse_qs(parsed.query)
    
    # 계정 정보 추출
    account_info = parsed.path.lstrip('/')
    
    print(f"\n" + "="*60)
    print(f"🔐 추출된 OTP 정보 상세 분석")
    print(f"="*60)
    
    print(f"📋 계정: {account_info}")
    print(f"🔗 URI 스키마: {parsed.scheme}")
    print(f"🔗 URI 호스트: {parsed.netloc}")
    print(f"🔗 URI 경로: {parsed.path}")
    print(f"🔗 URI 쿼리: {parsed.query}")
    
    # 모든 파라미터 출력
    print(f"\n📊 모든 파라미터:")
    for key, value in params.items():
        print(f"  {key}: {value[0] if value else 'None'}")
    
    if 'secret' in params:
        secret = params['secret'][0]
        
        print(f"\n🔑 Secret Key 상세 분석:")
        print(f"  원본 키: '{secret}'")
        print(f"  키 길이: {len(secret)} 문자")
        print(f"  키 타입: {type(secret)}")
        
        # 문자별 분석
        print(f"  문자 구성: {list(secret)}")
        
        # 대소문자 확인
        has_upper = any(c.isupper() for c in secret)
        has_lower = any(c.islower() for c in secret)
        has_digit = any(c.isdigit() for c in secret)
        has_special = any(not c.isalnum() for c in secret)
        
        print(f"  대문자 포함: {has_upper}")
        print(f"  소문자 포함: {has_lower}")
        print(f"  숫자 포함: {has_digit}")
        print(f"  특수문자 포함: {has_special}")
        
        # Base32 유효 문자 확인
        base32_chars = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
        secret_upper = secret.upper()
        invalid_chars = [c for c in secret_upper if c not in base32_chars]
        
        if invalid_chars:
            print(f"  ❌ Base32에 없는 문자: {invalid_chars}")
        else:
            print(f"  ✅ 모든 문자가 Base32 유효")
        
        # Base32 패딩 확인
        padding_needed = 8 - (len(secret) % 8)
        if padding_needed == 8:
            padding_needed = 0
        print(f"  Base32 패딩 필요: {padding_needed}개의 '='")
        
        # Base32 디코딩 시도
        try:
            import base64
            padded_secret = secret_upper + '=' * padding_needed
            decoded = base64.b32decode(padded_secret)
            print(f"  ✅ Base32 디코딩 성공")
            print(f"  디코딩된 바이트 수: {len(decoded)}")
            print(f"  디코딩된 비트 수: {len(decoded) * 8}")
            print(f"  16진수: {decoded.hex().upper()}")
            
        except Exception as e:
            print(f"  ❌ Base32 디코딩 실패: {e}")
        
        # 키 길이 기준 분석
        print(f"\n📏 키 길이 표준 분석:")
        print(f"  현재 길이: {len(secret)} 문자")
        print(f"  RFC 4226 최소 요구: 26 문자 (128비트)")
        print(f"  RFC 4226 권장: 32 문자 (160비트)")
        print(f"  Google Auth 최소: 16 문자")
        print(f"  실제 비트 수: {len(secret) * 5} 비트 (Base32)")
        
        if len(secret) < 16:
            print(f"  ❌ Google Authenticator 호환 불가")
            print(f"  💡 이는 QR 코드 생성 측의 문제일 수 있습니다")
            print(f"  💡 일부 서비스는 짧은 키를 사용하지만 표준 위반입니다")
        elif len(secret) < 26:
            print(f"  ⚠️  RFC 표준 미만이지만 일부 앱에서 작동할 수 있음")
        else:
            print(f"  ✅ 표준 준수")
            
        # 공백 제거 및 정리
        clean_secret = secret.replace(' ', '').replace('-', '').upper()
        if clean_secret != secret:
            print(f"\n🧹 정리된 키: {clean_secret}")
            secret = clean_secret
        
        print(f"⚠️  이 키는 절대 타인에게 노출하지 마세요!")
        
        # QR 코드 자체 문제 진단
        if len(secret) < 16:
            print(f"\n🔍 QR 코드 진단:")
            print(f"  - 해당 QR 코드는 실제로 {len(secret)}자 키를 포함")
            print(f"  - 이는 이미지 해상도 문제가 아님")
            print(f"  - QR 코드 생성 서비스에서 짧은 키를 사용함")
            print(f"  - 표준을 준수하지 않는 구현일 가능성")
            
    else:
        print(f"❌ Secret 키를 찾을 수 없습니다!")
        return
    
    # 나머지 파라미터들
    issuer = params.get('issuer', ['Unknown'])[0]
    algorithm = params.get('algorithm', ['SHA1'])[0]
    digits = params.get('digits', ['6'])[0]
    period = params.get('period', ['30'])[0]
    
    print(f"\n🏢 발급자: {issuer}")
    print(f"🔧 알고리즘: {algorithm}")
    print(f"🔢 자릿수: {digits}")
    print(f"⏰ 갱신주기: {period}초")
    
    # Google Authenticator 수동 입력 시도
    print(f"\n" + "="*60)
    print(f"📱 Google Authenticator 수동 입력 시도")
    print(f"="*60)
    
    if ':' in account_info:
        service, user = account_info.split(':', 1)
        print(f"서비스: {service}")
        print(f"계정: {user}")
    else:
        print(f"계정 이름: {account_info}")
    
    print(f"키: {secret}")
    print(f"시간 기준: 예")
    print(f"유형: TOTP")
    
    if len(secret) < 16:
        print(f"\n❌ 경고: 이 키는 Google Authenticator에서 거부될 가능성이 높습니다")
        print(f"💡 대안:")
        print(f"   1. 다른 OTP 앱 사용 (Authy, Microsoft Authenticator 등)")
        print(f"   2. 서비스 제공자에게 문의하여 더 긴 키 요청")
        print(f"   3. 서비스에서 다시 QR 코드 생성")
    else:
        print(f"\n✅ 이 키는 Google Authenticator에서 정상 작동할 것입니다")
    
    print(f"\n✅ 분석 완료!")

# 간단 실행
if __name__ == "__main__":
    print("🔍 Google OTP QR 코드 분석기")
    print("-" * 30)
    
    # 파일 선택 대화상자
    try:
        import tkinter as tk
        from tkinter import filedialog
        
        # tkinter 창 숨기기
        root = tk.Tk()
        root.withdraw()
        root.attributes('-topmost', True)  # 창을 맨 앞으로
        
        print("📁 파일 선택 창이 열립니다...")
        
        # 파일 선택
        image_path = filedialog.askopenfilename(
            title="QR 코드 이미지를 선택하세요",
            filetypes=[
                ("이미지 파일", "*.png *.jpg *.jpeg *.bmp *.gif *.webp"),
                ("PNG 파일", "*.png"),
                ("JPG 파일", "*.jpg *.jpeg"), 
                ("모든 파일", "*.*")
            ]
        )
        
        root.destroy()
        
        if image_path:
            print(f"선택된 파일: {image_path}")
            extract_otp_from_image(image_path)
        else:
            print("❌ 파일이 선택되지 않았습니다.")
            
    except ImportError:
        # tkinter 없으면 기존 방식
        print("파일 선택 창을 사용할 수 없습니다. 직접 입력하세요.")
        image_path = input("QR 코드 이미지 파일 경로: ").strip().strip('"')
        
        if image_path:
            extract_otp_from_image(image_path)
        else:
            print("❌ 파일 경로가 입력되지 않았습니다.")

+ Recent posts