C언어 다중 채팅 제작
2025. 7. 21. 15:13

 

 

 

 

 

 

 

 

 

 

 

 

 

🌟 전체 구조 개요

더보기

시스템 아키텍쳐

클라이언트 (cl.c) ←→ 소켓 통신 ←→ 서버 (serv.c) ←→ MySQL DB
  • 클라이언트: 사용자 인터페이스 및 서버의 통신 담당
  • 서버: 다중 클라이언트 관리, 채팅 중계, 데이터베이스 연동
  • 데이터베이스: 사용자 정보, 채팅 로그 저장

 

클라이언트 아키텍쳐 개요

사용자 입력 → 송신 스레드 → 소켓 → 서버
              ↑               ↓
         메인 스레드   수신 스레드 → 화면 출력

 

 

클라이언트 상태 머신

초기상태 → 로그인/회원가입 → 메인메뉴 → 개인정보변경 or 채팅
   ↑                      ↑           ↓
   └── 종료 ←── 로그아웃 ←───┘         /exit

 

 

 

🌟 서버 코드 분석

더보기

핵심 헤더 파일과 라이브러리

#include <stdio.h>          // 표준 입출력
#include <stdlib.h>         // 동적 메모리 할당
#include <string.h>         // 문자열 처리
#include <unistd.h>         // UNIX 시스템 호출
#include <pthread.h>        // 스레드 처리
#include <arpa/inet.h>      // 네트워크 통신
#include <mysql/mysql.h>    // MySQL 데이터베이스
#include <openssl/sha.h>    // SHA-256 암호화

 

 

전역 상수 정의

#define MAX_CLIENTS 100     // 최대 동시 접속 클라이언트 수
#define MAX_ROOMS 50        // 최대 채팅방 수
#define BUF_SIZE 1024       // 버퍼 크기
#define ID_LEN 50           // ID/이름 최대 길이
#define ROOM_NAME_LEN 50    // 방 이름 최대 길이

 

 

클라이언트 구조체

typedef struct {
    int sock;                    // 클라이언트 소켓 번호
    char username[ID_LEN];       // 사용자 ID
    char name[ID_LEN];          // 사용자 닉네임
    char room[ROOM_NAME_LEN];   // 현재 있는 채팅방
    int is_admin;               // 관리자 권한 (1: 관리자, 0: 일반)
    int is_muted;               // 음소거 상태 (1: 음소거, 0: 정상)
    pthread_t tid;              // 스레드 ID
} Client;

 

 

채팅방 구조체

typedef struct {
    char name[ID_LEN];      // 방 이름
    int member_count;       // 현재 방에 있는 사용자 수
} ChatRoom;

 

 

전역변수

Client* clients[MAX_CLIENTS];           // 클라이언트 포인터 배열
ChatRoom chat_rooms[MAX_ROOMS];         // 채팅방 배열
int client_count = 0;                   // 현재 접속 클라이언트 수
int room_count = 0;                     // 현재 생성된 방 수
pthread_mutex_t mutex;                  // 뮤텍스 (동시성 제어)
MYSQL* conn;                           // MySQL 연결 객체

 

 

비밀번호 해싱 (SHA-256)

void hash_256_password(const char* password, char* hashed_output) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((const unsigned char*)password, strlen(password), hash);
    
    // 16진수 문자열로 변환
    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        sprintf(hashed_output + (i * 2), "%02x", hash[i]);
    }
    hashed_output[64] = '\0';
}

 

 

ID 검증 (소문자+숫자+8자 이상)

int is_valid_id(const char* id) {
    if (strlen(id) < 8) return 0;
    
    int has_lower = 0, has_digit = 0;
    
    for (int i = 0; id[i]; ++i) {
        if (islower(id[i])) has_lower = 1;
        else if (isdigit(id[i])) has_digit = 1;
        else return 0;  // 소문자나 숫자가 아니면 실패
    }
    
    return has_lower && has_digit;
}

 

 

PW 검증 (대소문자+숫자+8자 이상)

int is_valid_password(const char* pw) {
    int len = strlen(pw);
    int upper = 0, lower = 0, digit = 0;
    
    if (len < 8) return 0;
    
    for (int i = 0; i < len; ++i) {
        if (isupper(pw[i])) upper = 1;
        if (islower(pw[i])) lower = 1;
        if (isdigit(pw[i])) digit = 1;
    }
    
    return upper && lower && digit;
}

 

 

회원가입 처리

int db_signup(const char* name, const char* username, const char* pw) {
    char hashed_pw[65];
    hash_256_password(pw, hashed_pw);  // 비밀번호 해싱
    
    // SQL 인젝션 방지를 위한 이스케이프 처리
    char escaped_name[ID_LEN * 2 + 1];
    char escaped_username[ID_LEN * 2 + 1];
    char escaped_pw[ID_LEN * 2 + 1];
    
    mysql_real_escape_string(conn, escaped_name, name, strlen(name));
    mysql_real_escape_string(conn, escaped_username, username, strlen(username));
    mysql_real_escape_string(conn, escaped_pw, hashed_pw, strlen(hashed_pw));
    
    // INSERT 쿼리 실행
    snprintf(query, sizeof(query),
        "INSERT INTO users (username, password, name, created_at, is_admin) "
        "VALUES('%s', '%s', '%s', NOW(), 0)", 
        escaped_username, escaped_pw, escaped_name);
    
    return mysql_query(conn, query) == 0;
}

 

 

로그인 검증

int db_login(const char* username, const char* pw) {
    char hashed_pw[65];
    hash_256_password(pw, hashed_pw);  // 입력 비밀번호 해싱
    
    // 해싱된 비밀번호로 SELECT 쿼리
    snprintf(query, sizeof(query),
        "SELECT * FROM users WHERE username='%s' AND password='%s'",
        escaped_username, escaped_pw);
    
    MYSQL_RES* res = mysql_store_result(conn);
    int success = mysql_num_rows(res) > 0;
    mysql_free_result(res);
    
    return success;
}

 

 

멀티 스레드 (연결 수락)

while (1) {
    // 새로운 클라이언트 연결 수락
    int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
    
    // 클라이언트 구조체 할당
    Client* cli = malloc(sizeof(Client));
    cli->sock = clnt_sock;
    
    // 새로운 스레드 생성
    pthread_create(&cli->tid, NULL, chat_session, (void*)cli);
    pthread_detach(cli->tid);  // 자동 메모리 해제
}

 

 

클라이언트 처리 스레드 (사용자 기준에 맞춰서)

void* chat_session(void *arg) {
    Client *cli = (Client*)arg;
    
    // 1. 로그인/회원가입 처리
    // 2. 메인 메뉴 처리
    // 3. 채팅 처리
    // 4. 정리 작업
    
    return NULL;
}

 

 

Broadcast

void broadcast_to_room(const char* roomname, const char* msg, int except_sock) {
    pthread_mutex_lock(&mutex);
    
    for (int i = 0; i < MAX_CLIENTS; ++i) {
        if (clients[i] && 
            strcmp(clients[i]->room, roomname) == 0 && 
            clients[i]->sock != except_sock) {
            send_with_length(clients[i]->sock, msg);
        }
    }
    
    pthread_mutex_unlock(&mutex);
}

 

 

귓속말 기능

void whisper(const char *from_name, const char *to_name, const char *body, int sender_sock) {
    Client* target_cli = find_client_by_name(to_name);
    
    if (target_cli == NULL) {
        send_with_length(sender_sock, "[서버] 사용자를 찾을 수 없습니다.\n");
    } else {
        char fmt[BUF_SIZE];
        snprintf(fmt, sizeof(fmt), "[귓속말] %s → %s: %s\n", from_name, to_name, body);
        send_with_length(target_cli->sock, fmt);
    }
}

 

 

관리자 기능

// 강제 퇴장
void admin_kick(int sender_sock, const char* target_name) {
    Client* target_cli = find_client_by_name(target_name);
    if (target_cli) {
        send_with_length(target_cli->sock, "[서버] 관리자에 의해 추방되었습니다.\n");
        close(target_cli->sock);  // 연결 강제 종료
    }
}


// 음소거
void admin_mute(int sender_sock, const char* target_name) {
    Client* target_cli = find_client_by_name(target_name);
    if (target_cli) {
        target_cli->is_muted = 1;
        send_with_length(target_cli->sock, "[서버] 음소거되었습니다.\n");
    }
}


// 공지
void admin_announce(const char* msg) {
    pthread_mutex_lock(&mutex);
    char full_msg[BUF_SIZE];
    snprintf(full_msg, sizeof(full_msg), "[공지] %s\n", msg);
    for (int i = 0; i < MAX_CLIENTS; ++i) {
        if (clients[i] != NULL) {
            send_with_length(clients[i]->sock, full_msg);
        }
    }
    pthread_mutex_unlock(&mutex);
}

 

 

뮤텍스를 통한 임계구역 보호

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 클라이언트 배열 접근 시
pthread_mutex_lock(&mutex);
for (int i = 0; i < MAX_CLIENTS; ++i) {
    if (clients[i] != NULL) {
        // 안전한 접근
    }
}
pthread_mutex_unlock(&mutex);

 

 

데이터베이스 로그 저장

int save_chat_log(const char* sender, const char* room, const char* message, 
                  int is_whisper, const char* recipients) {
    char query[2048];
    
    // SQL 인젝션 방지를 위한 이스케이프 처리
    mysql_real_escape_string(conn, escaped_sender, sender, strlen(sender));
    mysql_real_escape_string(conn, escaped_message, message, strlen(message));
    
    // 채팅 로그 INSERT
    snprintf(query, sizeof(query),
        "INSERT INTO chat_logs (sender, room, message, is_whisper, recipients, created_at) "
        "VALUES('%s', '%s', '%s', %d, '%s', NOW())", 
        escaped_sender, escaped_room, escaped_message, is_whisper, escaped_recipients);
    
    return mysql_query(conn, query) == 0;
}

 

 

주요 특징 요약

1. 보안성

  • SHA-256 비밀번호 해싱
  • SQL 인젝션 방지를 위한 이스케이프 처리
  • 입력 유효성 검사

2. 확장성

  • 모듈화된 구조
  • 최대 100명 동시 접속 지원
  • 채팅방 시스템

3. 사용성

  • 직관적인 명령어 시스템
  • 실시간 시간 표시
  • 깔끔한 UI/UX

4. 안정성

  • 멀티스레드 동시성 제어
  • 안전한 메모리 관리
  • 예외 상황 처리

5. 기능성

  • 개인정보 변경
  • 관리자 기능(킥, 뮤트, 공지)
  • 귓속말 시스템
  • 채팅방 생성/참여

 

 

 

🌟 클라이언트 코드 분석

더보기

전역 변수 상세 분석

int sock;                               // 서버와 연결된 소켓 (전역)
volatile int in_chat_mode = 0;          // 채팅 모드 여부 (0: 메뉴, 1: 채팅)
char username[ID_LEN];                  // 로그인 성공 후 사용자 ID
char name[ID_LEN];                      // 로그인 성공 후 사용자 닉네임
volatile int is_logged_in = 0;          // 로그인 상태 (-1: 종료, 0: 초기, 1: 성공)
volatile int input_active = 0;          // 사용자 입력 대기 중인지 여부
pthread_mutex_t write_mutex;            // 콘솔 출력 보호용 뮤텍스
pthread_mutex_t sock_write_mutex;       // 소켓 쓰기 보호용 뮤텍스

// volatile 사용 이유: 컴파일러 최적화 방지
// - 여러 스레드가 동시에 접근하는 변수들이므로 캐시가 아닌 메모리에서 직접 읽도록 보장

 

 

클라이언트의 3단계 스레드 구조

// main 스레드
int main(int argc, char *argv[]) {
    // 1. 소켓 생성 및 서버 연결
    sock = socket(PF_INET, SOCK_STREAM, 0);
    connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
    
    // 2. 수신 스레드 생성 (가장 먼저 실행)
    pthread_create(&recv_thread, NULL, recv_msg, NULL);
    pthread_detach(recv_thread);
    
    // 3. 로그인/회원가입 처리 (블로킹)
    int login_success = client_login_process(sock);
    
    // 4. 로그인 성공 시 송신 스레드 생성
    if (login_success) {
        pthread_create(&send_thread, NULL, send_msg, NULL);
        pthread_detach(send_thread);
    }
    
    // 5. 종료 플래그까지 대기
    while(is_logged_in != -1) {
        sleep(1);
    }
    
    return 0;
}


// 수신 스레드
void* recv_msg(void* arg) {
    char msg[BUF_SIZE];
    
    while (1) {
        // 서버로부터 메시지 수신
        int str_len = receive_message(sock, msg);
        if (str_len == 0) {
            // 서버 연결 끊김 처리
            pthread_mutex_lock(&write_mutex);
            if (input_active) {
                printf("\r\033[2K");  // 현재 입력 라인 지우기
            }
            printf("\n [서버] 서버와의 연결이 끊겼습니다.\n");
            pthread_mutex_unlock(&write_mutex);
            is_logged_in = -1;  // 종료 플래그 설정
            break;
        }

        // 핵심: 동시 입력 중일 때 깔끔한 출력 처리
        pthread_mutex_lock(&write_mutex);
        if (input_active) {
            printf("\r\033[2K");    // ANSI: 현재 라인 지우기
            printf("%s", msg);      // 서버 메시지 출력
            fflush(stdout);         // 즉시 출력
        } else {
            printf("%s", msg);      // 일반 출력
            fflush(stdout);
        }
        pthread_mutex_unlock(&write_mutex);

        // 로그인 성공 메시지 파싱
        if (strstr(msg, " 로그인 성공!") != NULL) {
            is_logged_in = 1;
            
            // 메시지에서 사용자 정보 추출
            // 형식: " 로그인 성공!username:nickname\n"
            char *exclamation = strstr(msg, " 로그인 성공!");
            if (exclamation != NULL) {
                char *colon = strchr(exclamation + strlen(" 로그인 성공!"), ':');
                if (colon != NULL) {
                    // username 추출
                    int username_len = colon - (exclamation + strlen(" 로그인 성공!"));
                    strncpy(username, exclamation + strlen(" 로그인 성공!"), username_len);
                    username[username_len] = '\0';
                    
                    // nickname 추출
                    char *name_start = colon + 1;
                    char *newline = strchr(name_start, '\n');
                    int name_len = newline - name_start;
                    strncpy(name, name_start, name_len);
                    name[name_len] = '\0';
                }
            }
        }
        
        // 채팅 모드 상태 변경 감지
        if (strstr(msg, " 채팅을 종료하고 메인 메뉴로 돌아갑니다") != NULL) {
            in_chat_mode = 0;
        }
    }
    return NULL;
}

// 송신 스레드
void* send_msg(void* arg) {
    char buf[BUF_SIZE];
    int in_personal_menu = 0;       // 개인정보 변경 메뉴
    int expecting_confirmation = 0;  // Y/N 확인 대기
    int expecting_new_data = 0;     // 새 데이터 입력 대기
    
    // 로그인 완료까지 대기 (바쁜 대기 방지)
    while (is_logged_in == 0) {
        if (is_logged_in == -1) return NULL;  // 종료 감지
        usleep(100000);  // 100ms 대기
    }

    // 메인 루프: 사용자 입력 처리
    while (1) {
        if (is_logged_in == -1) break;  // 종료 플래그 확인

        // 입력 상태 플래그 설정 (수신 스레드가 감지)
        pthread_mutex_lock(&write_mutex);
        input_active = 1;
        pthread_mutex_unlock(&write_mutex);

        // 사용자 입력 받기
        if (fgets(buf, BUF_SIZE, stdin) == NULL) {
            fprintf(stderr, "입력 오류 발생\n");
            break;
        }
        buf[strcspn(buf, "\n")] = 0;  // 개행 문자 제거

        // 입력 상태 플래그 해제
        pthread_mutex_lock(&write_mutex);
        input_active = 0;
        pthread_mutex_unlock(&write_mutex);

        // 상태별 입력 처리 (상태 머신 패턴)
        if (!in_personal_menu && !in_chat_mode) {
            // 메인 메뉴 상태
            if (strcmp(buf, "1") == 0) {
                in_personal_menu = 1;  // 개인정보 변경 모드
            } else if (strcmp(buf, "2") == 0) {
                in_chat_mode = 1;      // 채팅 모드
            }
            
        } else if (in_personal_menu) {
            // 개인정보 변경 메뉴 상태
            if (strcmp(buf, "5") == 0) {
                in_personal_menu = 0;  // 메인 메뉴로 복귀
            } else if (strcmp(buf, "1") == 0 || strcmp(buf, "2") == 0 || strcmp(buf, "3") == 0) {
                expecting_confirmation = 1;  // Y/N 확인 단계
            } else if (expecting_confirmation) {
                if (strcmp(buf, "Y") == 0 || strcmp(buf, "y") == 0) {
                    expecting_confirmation = 0;
                    expecting_new_data = 1;  // 새 데이터 입력 단계
                } else if (strcmp(buf, "N") == 0 || strcmp(buf, "n") == 0) {
                    expecting_confirmation = 0;  // 취소
                }
            } else if (expecting_new_data) {
                expecting_new_data = 0;  // 입력 완료
            }
            
        } else if (in_chat_mode) {
            // 채팅 모드 상태
            if (strcmp(buf, "/exit") == 0) {
                // 채팅 종료는 서버 응답에 따라 처리
            } else {
                // 채팅 메시지 처리
                char time_str[16];
                get_current_time(time_str);
                
                if (strncmp(buf, "/w ", 3) == 0) {
                    // 귓속말 처리
                    char target_name[ID_LEN], body[BUF_SIZE];
                    if (sscanf(buf + 3, "%49s %1023[^\n]", target_name, body) >= 2) {
                        printf("%s [귓속말 전송] %s에게: %s\n", time_str, target_name, body);
                    }
                } else if (strncmp(buf, "/", 1) != 0) {
                    // 일반 채팅 메시지 (명령어가 아닌 경우)
                    printf("%s [%s] %s\n", time_str, name, buf);
                }
                // 명령어들은 서버에서 응답이 와서 출력됨
            }
        }
        
        // 서버로 메시지 전송
        send_with_length(sock, buf);
    }
    return NULL;
}

 

 

상태별 처리 로직

// 메인 메뉴 상태
if (!in_personal_menu && !in_chat_mode) {
    // "1": 개인정보 변경, "2": 채팅 시작, "3": 로그아웃
}

// 개인정보 변경 상태
else if (in_personal_menu) {
    // "1": ID변경, "2": PW변경, "3": 닉네임변경, "4": 회원탈퇴, "5": 돌아가기
    // Y/N 확인 → 새 데이터 입력 → 완료
}

// 채팅 상태
else if (in_chat_mode) {
    // "/exit": 채팅 종료, "/w": 귓속말, "/help": 도움말
    // 일반 메시지: 시간과 함께 출력
}

 

 

동시 입출력 처리

// 수신 스레드에서:
if (input_active) {
    printf("\r\033[2K");    // \r: 커서를 줄 시작으로, \033[2K: 현재 줄 지우기
    printf("%s", msg);      // 서버 메시지 출력
    fflush(stdout);         // 버퍼 비우기
}

 

 

시간 표시 기능

void get_current_time(char* time_str) {
    time_t now;
    struct tm* local_time;
    
    time(&now);                                    // 현재 시간 구하기
    local_time = localtime(&now);                  // 지역 시간으로 변환
    sprintf(time_str, "[%02d:%02d]",              // HH:MM 형식
            local_time->tm_hour, 
            local_time->tm_min);
}

 

 

ASCII 아트 헤더

void print_fixed_header() {
    // 유니코드 블록 문자를 사용한 아스키 아트
    // 브레일 패턴 문자들로 그림 그리기
    printf("⠀⠀⠀⠀⠀⠀⠀...\n");  // 공백 블록
    printf("        ⠀⠀⠀...\n");     // 들여쓰기 + 패턴
}

 

 

초기화 및 로그인 프로세스

int client_login_process(int sock) {
    char input_buf[BUF_SIZE];
    
    // 로그인 성공 또는 프로그램 종료까지 루프
    while (is_logged_in == 0) {
        fgets(input_buf, BUF_SIZE, stdin);
        input_buf[strcspn(input_buf, "\n")] = 0;
        send_with_length(sock, input_buf);

        if (strcmp(input_buf, "1") == 0) {
            // 회원가입 프로세스
            int signup_completed = 0;
            while (!signup_completed && is_logged_in == 0) {
                fgets(input_buf, BUF_SIZE, stdin);
                input_buf[strcspn(input_buf, "\n")] = 0;
                send_with_length(sock, input_buf);
                usleep(200000);  // 서버 응답 대기
            }
            
        } else if (strcmp(input_buf, "2") == 0) {
            // 로그인 프로세스
            while(is_logged_in == 0) {
                fgets(input_buf, BUF_SIZE, stdin);
                input_buf[strcspn(input_buf, "\n")] = 0;
                send_with_length(sock, input_buf);
                usleep(100000);  // 서버 응답 대기
            }
            break;  // 로그인 성공
            
        } else if (strcmp(input_buf, "3") == 0) {
            // 종료 처리
            // Y/N 확인 후 처리
        }
    }
    
    return (is_logged_in == 1) ? 1 : 0;
}

 

 

뮤텍스 사용 패턴

// 1. 콘솔 출력 보호
pthread_mutex_lock(&write_mutex);
printf("안전한 출력\n");
pthread_mutex_unlock(&write_mutex);

// 2. 소켓 쓰기 보호
pthread_mutex_lock(&sock_write_mutex);
write(sock, data, size);
pthread_mutex_unlock(&sock_write_mutex);

 

 

안전한 메시지 전송

void send_with_length(int target_sock, const char* msg) {
    pthread_mutex_lock(&sock_write_mutex);  // 소켓 쓰기 보호
    
    int msg_len = strlen(msg) + 1;          // 널 문자 포함
    int net_msg_len = htonl(msg_len);       // 네트워크 바이트 순서
    
    // 길이 전송
    if (write(target_sock, &net_msg_len, sizeof(net_msg_len)) != sizeof(net_msg_len)) {
        perror("Failed to send message length");
        pthread_mutex_unlock(&sock_write_mutex);
        return;
    }
    
    // 메시지 전송
    if (write(target_sock, msg, msg_len) != msg_len) {
        perror("Failed to send message body");
        pthread_mutex_unlock(&sock_write_mutex);
        return;
    }
    
    pthread_mutex_unlock(&sock_write_mutex);  // 보호 해제
}

 

 

주요 특징 요약

1. 분리된 관심사

  • 수신 스레드: 서버 메시지 처리 전담
  • 송신 스레드: 사용자 입력 처리 전담
  • 메인 스레드: 연결 관리 및 스레드 조율

2. 상태 기반 처리

  • 현재 상태에 따라 사용자 입력을 다르게 해석
  • 명확한 상태 전이 규칙

3. 비동기 I/O

  • 입력과 출력이 서로 블로킹 X
  • 실시간 채팅 제공

4. 종료

  • 연결 끊김 감지 및 정리
  • 메모리 누수 방지

 

 

 

🌟 client code

더보기
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <sys/select.h> // select 함수를 위해 추가
#include <time.h>       // 시간 관련 함수 추가

#define BUF_SIZE 1024
#define ID_LEN 50 // 서버와 동일하게 ID_LEN 정의
#define XOR_KEY 0x5A // XOR 암호화 키 (예시로 사용, 실제로는 더 안전한 키를 사용해야 함)

// 전역 변수
int sock; // 서버와 연결된 소켓
volatile int in_chat_mode = 0;
char username[ID_LEN]; // 로그인 성공 후 사용자 ID 저장
char name[ID_LEN];     // 로그인 성공 후 사용자 이름(닉네임) 저장
volatile int is_logged_in = 0; // 로그인 상태 플래그 (-1: 종료, 0: 초기 메뉴, 1: 로그인 성공)
pthread_mutex_t write_mutex = PTHREAD_MUTEX_INITIALIZER; // 콘솔 출력 보호용 뮤텍스
pthread_mutex_t sock_write_mutex = PTHREAD_MUTEX_INITIALIZER; // 소켓\ 쓰기 보호용 뮤텍스

// 사용자 입력 상태 플래그 (volatile로 선언하여 컴파일러 최적화 방지)
// send_msg 스레드가 사용자 입력을 기다리는 중인지 여부를 나타냅니다.
volatile int input_active = 0; 


// 현재 시간을 [시:분] 형태로 반환하는 함수
void get_current_time(char* time_str) {
    time_t now;
    struct tm* local_time;
    
    time(&now);
    local_time = localtime(&now);
    
    sprintf(time_str, "[%02d:%02d]", local_time->tm_hour, local_time->tm_min);
}

// 에러 출력 및 프로그램 종료 함수
void error_handling(const char *msg) {
    perror(msg);
    exit(1);
}

// 아스키 화면 고정
// 아스키 화면 고정
void print_fixed_header() 
{
    printf("\n");
    printf("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⡟⣍⠍⡩⡩⢉⠍⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠙⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢘⡇⠈⠀⠈⠁⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢘⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⠶⠞⠛⠛⠛⠛⠛⠛⠳⠶⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢧⣀⣀⣀⣀⡀⠀⠀⢀⣀⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠹⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⢹⡇⣀⡴⠛⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢘⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠿⠋⠀⢸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⢦⣄⣀⠀⠀⠀⠀⠀⠀⢀⠀⠀⢸⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠛⠛⠛⠛⠛⠛⠛⠛⢶⣌⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣙⠍⡙⢍⣿⢹⠹⡹⡹⢙⢽⢽⢹⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣶⣦⣷⣼⣿⣾⣼⣶⣵⣿⣼⣾⣼⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⢠⡤⠀⣀⣀⣀⣀⡀⣤⠀⠀⣤⣤⣤⠀⢠⡄⠀⢀⣀⣀⣀⡀⢠⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠂⠀⠰⣿⠀⠈⢹⣿⡉⠀⣿⣤⠰⠶⠶⠶⠶⢸⡇⠀⠀⠉⠉⣿⠂⢸⡧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣄⠀⢘⣯⠀⠰⠟⠉⠷⠀⣿⠀⠀⣾⠛⢳⡄⢸⡟⠃⠀⠀⢀⣿⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⡾⠃⠹⡦⢘⣯⠀⠀⠛⠛⠛⠛⣿⠀⠀⠻⣤⠾⠃⢸⡇⠀⠠⣴⠞⠃⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("        ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠋⠀⠀⠀⠀⠀⠀⠈⠃⠀⠀⠀⠀⠀⠀⠈⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n");
    printf("\n"); // 아스키 아트 뒤에 줄 바꿈 추가
}

void send_with_length(int target_sock, const char* msg) {
    pthread_mutex_lock(&sock_write_mutex); // 소켓 쓰기 보호
    int msg_len = strlen(msg) + 1; // 널 종료 문자 포함
    int net_msg_len = htonl(msg_len);
    if (write(target_sock, &net_msg_len, sizeof(net_msg_len)) != sizeof(net_msg_len)) {
        perror("Failed to send message length");
        pthread_mutex_unlock(&sock_write_mutex);
        return;
    }
    if (write(target_sock, msg, msg_len) != msg_len) {
        perror("Failed to send message body");
        pthread_mutex_unlock(&sock_write_mutex);
        return;
    }
    pthread_mutex_unlock(&sock_write_mutex); // 소켓 쓰기 보호 해제
}

// 메시지를 길이 기반으로 안전하게 전송하는 함수
// 사용자 입력을 받아 서버로 전송하는 스레드 함수 (로그인 후 메인 메뉴 및 채팅 전용)
void* send_msg(void* arg) {
    char buf[BUF_SIZE];
    int in_personal_menu = 0; // 개인정보 변경 메뉴에 있는지 여부
    int expecting_confirmation = 0; // Y/N 확인을 기다리는 중인지
    int expecting_new_data = 0; // 새로운 데이터 입력을 기다리는 중인지
    
    // 로그인/회원가입 프로세스가 완료될 때까지 대기
    while (is_logged_in == 0) {
        if (is_logged_in == -1) return NULL; // 프로그램 종료 플래그 감지
        usleep(100000); // 100ms 대기 (바쁜 대기 방지)
    }

    if (is_logged_in == -1) return NULL; // 대기 중 프로그램 종료 시 스레드 종료

    // 메인 메뉴 및 채팅 메시지 전송 루프
    while (1) {
        if (is_logged_in == -1) break; // 수신 스레드가 종료 플래그 설정 시 종료

        pthread_mutex_lock(&write_mutex);
        fflush(stdout);
        input_active = 1; // 입력 대기 상태 설정
        pthread_mutex_unlock(&write_mutex);

        if (fgets(buf, BUF_SIZE, stdin) == NULL) {
            fprintf(stderr, "입력 오류 발생\n");
            break;
        }
        buf[strcspn(buf, "\n")] = 0; // 개행 문자 제거

        pthread_mutex_lock(&write_mutex);
        input_active = 0; // 입력 대기 상태 해제
        pthread_mutex_unlock(&write_mutex);

        // 채팅 모드 감지 및 상태 관리
         if (!in_personal_menu && !in_chat_mode) {
            // 메인 메뉴에서
            if (strcmp(buf, "1") == 0) {
                in_personal_menu = 1; // 개인정보 변경 메뉴 진입
            } else if (strcmp(buf, "2") == 0) {
                in_chat_mode = 1; // 채팅 모드 진입
            }
        } else if (in_personal_menu) {
            // 개인정보 변경 메뉴에서
            if (strcmp(buf, "5") == 0) {
                in_personal_menu = 0; // 메인 메뉴로 돌아감
            } else if (strcmp(buf, "1") == 0 || strcmp(buf, "2") == 0 || strcmp(buf, "3") == 0) {
                expecting_confirmation = 1; // 다음에 Y/N 확인이 올 것임
            } else if (expecting_confirmation) {
                if (strcmp(buf, "Y") == 0 || strcmp(buf, "y") == 0) {
                    expecting_confirmation = 0;
                    expecting_new_data = 1; // 새로운 데이터 입력 대기
                } else if (strcmp(buf, "N") == 0 || strcmp(buf, "n") == 0) {
                    expecting_confirmation = 0; // 취소
                }
            } else if (expecting_new_data) {
                expecting_new_data = 0; // 새로운 데이터 입력 완료
            }
        } else if (in_chat_mode) {
            // 채팅 모드에서
            if (strcmp(buf, "/exit") == 0) {
                // /exit는 방만 나가기 - 서버 응답 대기
                // 서버에서 응답을 받은 후 처리
            }else {
                // 현재 시간 가져오기
                char time_str[16];
                get_current_time(time_str);
                
                // 귓속말인지 확인
                if (strncmp(buf, "/w ", 3) == 0) {
                    // 귓속말 명령어 파싱
                    char target_name[ID_LEN], body[BUF_SIZE];
                    char *p = buf + 3;
                    if (sscanf(p, "%49s %1023[^\n]", target_name, body) >= 2) {
                        // 본인 화면에 귓속말 전송 메시지 출력 (시간 포함)
                        pthread_mutex_lock(&write_mutex);
                        printf("%s [귓속말 전송] %s에게: %s\n", time_str, target_name, body);
                        pthread_mutex_unlock(&write_mutex);
                    }
                } else if (strncmp(buf, "/kick ", 6) == 0 || 
                          strncmp(buf, "/mute ", 6) == 0 || 
                          strncmp(buf, "/announce ", 10) == 0) {
                    // 관리자 명령어는 시간 표시 안 함 (서버에서 응답 받음)
                } else if (strncmp(buf, "/", 1) == 0) {
                    // 기타 명령어들 (/help, /c, /j, /l 등)은 클라이언트에서 출력하지 않음
                    // 서버에서 응답을 받아서 출력
                } else {
                    pthread_mutex_lock(&write_mutex);
                    printf("%s [%s] %s\n", time_str, name, buf);
                    pthread_mutex_unlock(&write_mutex);
                }
            }
        }
        
        send_with_length(sock, buf);
        
        // 특정 명령어 입력 시 스레드 종료 처리
        if (strcmp(buf, "3") == 0 && !in_personal_menu && !in_chat_mode) { // 메인 메뉴에서 로그아웃
            // 로그아웃 확인 및 응답을 위해 바로 종료하지 않고 서버 응답을 기다림
            // recv_msg 스레드가 서버의 "로그아웃 합니다. 서버 연결을 종료합니다." 메시지를 받고
            // 연결 종료를 감지하여 is_logged_in을 -1로 설정할 것임
            // 따라서 여기서는 바로 break하지 않고 서버 응답을 기다림
        } else if (strcmp(buf, "y") == 0 || strcmp(buf, "Y") == 0) { 
            // 로그아웃 확인에서 Y를 선택한 경우
            // 서버가 연결을 종료할 것이므로 recv_msg 스레드가 감지할 것임
            // 따라서 여기서는 특별한 처리 없이 계속 진행
        }
    }
    return NULL;
}

// 메시지를 길이 기반으로 안전하게 수신하는 함수
int receive_message(int target_sock, char* out_msg) {
    int msg_len;
    ssize_t len, total = 0;

    // 메시지 길이 수신
    while (total < sizeof(msg_len)) {
        len = read(target_sock, ((char*)&msg_len) + total, sizeof(msg_len) - total);
        if (len <= 0) return 0; // 연결 종료 또는 오류
        total += len;
    }

    msg_len = ntohl(msg_len);
    if (msg_len <= 0 || msg_len >= BUF_SIZE) {
        fprintf(stderr, " Invalid message length received : %d\n", msg_len);
        return 0;
    }

    // 실제 메시지 수신
    total = 0;
    while (total < msg_len) {
        len = read(target_sock, out_msg + total, msg_len - total);
        if (len <= 0) return 0; // 연결 종료 또는 오류
        total += len;
    }
    out_msg[msg_len - 1] = '\0'; // 널 종료
    return 1;
}

// 서버로부터 메시지를 수신하여 출력하는 스레드 함수
void* recv_msg(void* arg) {
    char msg[BUF_SIZE];
    while (1) {
        int str_len = receive_message(sock, msg);
        if (str_len == 0) { // 서버 연결 종료
            pthread_mutex_lock(&write_mutex);
            // 만약 사용자가 입력 중이었다면, 입력 라인을 정리하고 종료 메시지 출력
            if (input_active) {
                printf("\r\033[2K"); // 현재 라인 지우기
            }
            printf("\n [서버] 서버와의 연결이 끊겼습니다. 프로그램을 종료합니다.\n");
            pthread_mutex_unlock(&write_mutex);
            is_logged_in = -1; // 종료 플래그 설정
            break;
        }

        pthread_mutex_lock(&write_mutex);
        // 사용자가 현재 입력 중이고 프롬프트가 화면에 있다면,
        // 현재 라인을 지우고 메시지를 출력한 다음 프롬프트를 다시 출력
        if (input_active) {
            printf("\r\033[2K"); // 현재 라인 지우기 (ANSI 이스케이프 코드)
            printf("%s", msg); // 받은 메시지 출력 (새 줄)
            fflush(stdout); // 즉시 출력
        } else {
            // 입력 중이 아니라면 그냥 메시지 출력
            printf("%s", msg);
            fflush(stdout);
        }
        pthread_mutex_unlock(&write_mutex);

        // --- 수정된 로그인 성공 메시지 처리 ---
        if (strstr(msg, " 로그인 성공!") != NULL) {
            is_logged_in = 1;
            
            // 로그인 성공 메시지에서 사용자 정보 추출
            char *exclamation = strstr(msg, " 로그인 성공!");
            if (exclamation != NULL) {
                char *colon = strchr(exclamation + strlen(" 로그인 성공!"), ':');
                if (colon != NULL) {
                    // username 추출 (! 다음부터 : 이전까지)
                    char *username_start = exclamation + strlen(" 로그인 성공!");
                    int username_len = colon - username_start;
                    if (username_len > 0 && username_len < ID_LEN) {
                        strncpy(username, username_start, username_len);
                        username[username_len] = '\0';
                    }
                    
                    // name 추출 (: 다음부터 \n 이전까지)
                    char *name_start = colon + 1;
                    char *newline = strchr(name_start, '\n');
                    if (newline != NULL) {
                        int name_len = newline - name_start;
                        if (name_len > 0 && name_len < ID_LEN) {
                            strncpy(name, name_start, name_len);
                            name[name_len] = '\0';
                        }
                    } else {
                        // 개행이 없는 경우 끝까지 복사
                        if (strlen(name_start) < ID_LEN) {
                            strcpy(name, name_start);
                            // 혹시 뒤에 공백이나 특수문자가 있다면 제거
                            name[strcspn(name, " \t\r\n")] = '\0';
                        }
                    }
                    
                    printf("[디버그] 파싱된 사용자 정보 - ID: '%s', 이름: '%s'\n", username, name);
                }
            }
        }
        
        if (strstr(msg, " 채팅을 종료하고 메인 메뉴로 돌아갑니다") != NULL) {
            in_chat_mode = 0; // 채팅 모드 종료
        }
        // 추가: /exit로 방을 나갔을 때는 채팅 모드 유지
        if (strstr(msg, " 방을 나가서 전체 채팅방으로 이동했습니다") != NULL) {
            // 채팅 모드는 유지, 단지 방만 변경됨
        }
    }
    return NULL;
}

// 클라이언트의 초기 로그인/회원가입 프로세스를 처리하는 함수
int client_login_process(int sock) {
    char input_buf[BUF_SIZE];
    
    // 이 루프는 is_logged_in이 1 (로그인 성공) 또는 -1 (종료)이 될 때까지 계속됩니다.
    while (is_logged_in == 0) {
        pthread_mutex_lock(&write_mutex);
        fflush(stdout);
        pthread_mutex_unlock(&write_mutex);

        if (fgets(input_buf, BUF_SIZE, stdin) == NULL) {
            fprintf(stderr, "입력 오류 발생\n");
            return 0; // 오류 발생 시 종료
        }
        input_buf[strcspn(input_buf, "\n")] = 0; // 개행 문자 제거

        send_with_length(sock, input_buf); // 서버로 메뉴 선택 전송

        if (strcmp(input_buf, "1") == 0) { // 회원가입 선택
            int signup_completed = 0;
            // 회원가입 프로세스
            while (!signup_completed && is_logged_in == 0) {
                pthread_mutex_lock(&write_mutex);
                fflush(stdout);
                pthread_mutex_unlock(&write_mutex);
                
                if (fgets(input_buf, BUF_SIZE, stdin) == NULL) return 0;
                input_buf[strcspn(input_buf, "\n")] = 0;
                send_with_length(sock, input_buf);
                
                // 회원가입 성공 메시지 확인을 위한 대기
                usleep(200000); // 200ms 대기
                
                // 서버로부터 메시지를 확인하여 회원가입 완료 여부 판단
                // recv_msg 스레드가 처리하므로 여기서는 단순히 대기
                // 회원가입이 완료되면 서버가 다시 메인 메뉴를 보낼 것임
            }
            // 회원가입 완료 후 다시 메인 메뉴로 돌아감
            continue;
            
        } else if (strcmp(input_buf, "2") == 0) { // 로그인 선택
            // ID, PW 입력 루프
            while(is_logged_in == 0) {
                pthread_mutex_lock(&write_mutex);
                fflush(stdout);
                pthread_mutex_unlock(&write_mutex);
                if (fgets(input_buf, BUF_SIZE, stdin) == NULL) return 0;
                input_buf[strcspn(input_buf, "\n")] = 0;
                send_with_length(sock, input_buf);
                usleep(100000); // 서버 응답 대기
            }
            break; // 로그인 완료
            
        } else if (strcmp(input_buf, "3") == 0) { // 종료 선택
            usleep(100000); // 100ms 대기
            
            pthread_mutex_lock(&write_mutex);
            // printf("> "); // 클라이언트 측 프롬프트
            fflush(stdout);
            pthread_mutex_unlock(&write_mutex);
            
            if (fgets(input_buf, BUF_SIZE, stdin) == NULL) return 0;
            input_buf[strcspn(input_buf, "\n")] = 0; // 개행 문자 제거
            
            send_with_length(sock, input_buf);
            
            usleep(100000); // 서버 응답 대기
            
            if (strcmp(input_buf, "y") == 0 || strcmp(input_buf, "Y") == 0) {
                return 0; // 종료를 나타냄
            }
            continue;
        } else {
            // 잘못된 입력, 서버가 다시 메뉴를 보낼 것이므로 루프를 계속합니다.
            usleep(100000); // 서버 응답 대기
        }
    }
    return (is_logged_in == 1) ? 1 : 0; // 로그인 성공 시 1, 그 외 0 반환
}


int main(int argc, char *argv[]) {
    struct sockaddr_in serv_adr;
    pthread_t send_thread, recv_thread;

    if (argc != 3) {
        printf("Usage : %s <IP> <PORT>\n", argv[0]);
        exit(1);
    }

    // 소켓 생성
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    // 서버 연결
    if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error");
    else
        printf("--------------------------------------------------------------------\n");
        printf("               [클라이언트] 서버에 연결되었습니다.\n");
        printf("--------------------------------------------------------------------\n");

    // 아스키 함수
    
    print_fixed_header();

    // 수신 스레드 생성
    pthread_create(&recv_thread, NULL, recv_msg, NULL);
    pthread_detach(recv_thread); // 스레드 자동 소멸 설정

    // 초기 로그인/회원가입 프로세스 처리
    // 이 함수는 로그인 성공 또는 프로그램 종료 시까지 블로킹됩니다.
    int login_success = client_login_process(sock);

    if (login_success) {
        // 로그인/회원가입이 성공적으로 완료되면,
        // 메인 메뉴 및 채팅 메시지 전송을 담당할 send_msg 스레드를 생성합니다.
        pthread_create(&send_thread, NULL, send_msg, NULL);
        pthread_detach(send_thread); // 스레드 자동 소멸 설정
    } else {
        // client_login_process에서 종료(3번 선택 또는 오류)되었다면,
        // 메인 루프를 종료하도록 is_logged_in을 -1로 설정합니다.
        is_logged_in = -1;
    }

    // 메인 스레드는 프로그램 종료 플래그가 설정될 때까지 대기
    while(is_logged_in != -1) {
        sleep(1); // 스레드들이 작업을 수행할 수 있도록 잠시 대기
    }
    
    // 소켓 닫기
    close(sock);
    return 0;
}

 

 

 

🌟 server code

더보기
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <mysql/mysql.h>
#include <ctype.h> // islower, isdigit, isupper 등을 위해 추가
#include <time.h>  // 시간 관련 함수 
#include <openssl/sha.h>  // SHA-256 해시 함수 사용

#define MAX_CLIENTS 100
#define MAX_ROOMS 50 // 현재 사용되지 않음, 확장 시 필요
#define BUF_SIZE 1024
#define ID_LEN 50   // ID (username) 및 이름(name) 길이
#define ROOM_NAME_LEN 50 // 방 이름 길이 

// MySQL DB 연결 정보 (! 사용자 환경에 맞게 수정해주세요 !)
#define DB_HOST "localhost"
#define DB_USER "root"
#define DB_PASS "개인 설정용 비번"       // 비밀번호
#define DB_NAME "개인 설정용 db"    // 데이터베이스 이름
#define DB_PORT 3306         // MySQL 기본 포트

// 컴파일링 : gcc serv.c -o server -lmysqlclient -lpthread -lcrypto

// 클라이언트 구조체 정의
typedef struct {
    int sock;
    char username[ID_LEN];
    char name[ID_LEN];
    char room[ROOM_NAME_LEN]; 
    int is_admin;
    int is_muted;
    pthread_t tid;
} Client;

// 채팅방 구조체 정의
typedef struct {
    char name[ID_LEN];      // 방 이름
    int member_count;
} ChatRoom;

// 현재 활성화된 채팅방을 관리하기 위한 배열
ChatRoom chat_rooms[MAX_ROOMS];

// 현재 활성화된 클라이언트 수를 관리하기 위한 변수
int room_count = 0;

// Client 포인터 배열 (동적 할당된 Client 구조체 포인터를 저장)
Client* clients[MAX_CLIENTS];
int client_count = 0; // 활성화된 클라이언트 수

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
MYSQL* conn;

// 에러 출력 및 프로그램 종료 함수
void error_handling(const char *msg) {
    perror(msg);
    exit(1);
}

// 현재 시간을 [시:분] 형태로 반환하는 함수
void get_current_time(char* time_str) {
    time_t now;
    struct tm* local_time;

    time(&now);
    local_time = localtime(&now);

    sprintf(time_str, "[%02d:%02d]", local_time->tm_hour, local_time->tm_min);
}

// UTF-8 문자열의 실제 글자 수를 계산하는 함수
int utf8_strlen(const char* s) {
    int len = 0;
    if (s == NULL) return 0; // NULL 포인터 체크
    while (*s) { // 포인터 s가 가리키는 값이 널 종료 문자가 아닐 때까지
        // 현재 바이트가 UTF-8 멀티바이트 문자의 첫 번째 바이트인지 확인
        // (상위 2비트가 10이 아닌 경우, 즉 0xxxxxxx, 11xxxxxx)
        if ((*s & 0xc0) != 0x80) {
            len++;  // 첫 바이트만 카운트
        }
        s++; // 다음 바이트로 이동
    }
    return len;
}

// 메시지를 길이 기반으로 안전하게 전송하는 함수
void send_with_length(int sock, const char* msg) {
    int msg_len = strlen(msg) + 1; // 널 종료 문자 포함
    int net_msg_len = htonl(msg_len);
    if (write(sock, &net_msg_len, sizeof(net_msg_len)) != sizeof(net_msg_len)) {
        perror(" Failed to send message length");
        return;
    }
    if (write(sock, msg, msg_len) != msg_len) {
        perror(" Failed to send message body");
        return;
    }
}

// 메시지를 길이 기반으로 안전하게 수신하는 함수
int receive_message(int sock, char* out_msg) {
    int msg_len;
    ssize_t len, total = 0;

    // 메시지 길이 수신
    while (total < sizeof(msg_len)) {
        len = read(sock, ((char*)&msg_len) + total, sizeof(msg_len) - total);
        if (len <= 0) return 0; // 연결 종료 또는 오류
        total += len;
    }

    msg_len = ntohl(msg_len);
    if (msg_len <= 0 || msg_len >= BUF_SIZE) { // msg_len이 너무 크거나 0 이하인 경우 방지
        fprintf(stderr, " Invalid message length received: %d\n", msg_len);
        return 0;
    }

    // 실제 메시지 수신
    total = 0;
    while (total < msg_len) {
        len = read(sock, out_msg + total, msg_len - total);
        if (len <= 0) return 0; // 연결 종료 또는 오류
        total += len;
    }
    out_msg[msg_len - 1] = '\0'; // 널 종료
    return 1;
}

//비밀번호를 SHA-256 해시로 변환하는 함수
void hash_256_password(const char* password, char* hashed_output) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((const unsigned char*)password, strlen(password), hash);
    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        sprintf(hashed_output + (i * 2), "%02x", hash[i]);
    }
    hashed_output[64] = '\0';  // 널 종료 문자
}


// MySQL에 사용자 회원가입 정보 삽입 함수
// name: 닉네임/이름, username: ID
int db_signup(const char* name, const char* username, const char* pw) {
    char query[512];
    char escaped_name[ID_LEN * 2 + 1];
    char escaped_username[ID_LEN * 2 + 1];
    char escaped_pw[ID_LEN * 2 + 1];

    // 1. 비밀번호 해싱
    char hashed_pw[65];  // SHA-256은 64자 + 널문자
    hash_256_password(pw, hashed_pw);

    printf(" [DEBUG] db_signup 호출됨: name='%s', username='%s', pw='%s'\n", name, username, pw);
    printf(" [DEBUG] 해시된 비밀번호: %s\n", hashed_pw);  // 디버그용

    // 2. escape 시 해시된 pw 사용
    mysql_real_escape_string(conn, escaped_name, name, strlen(name));
    mysql_real_escape_string(conn, escaped_username, username, strlen(username));
    mysql_real_escape_string(conn, escaped_pw, hashed_pw, strlen(hashed_pw));

    // 3. 해시된 비밀번호를 쿼리에 삽입
    snprintf(query, sizeof(query),
        "INSERT INTO users (username, password, name, created_at, is_admin) "
        "VALUES('%s', '%s', '%s', NOW(), 0)", escaped_username, escaped_pw, escaped_name);

    printf(" [DEBUG] 생성된 SQL 쿼리: %s\n", query);

    pthread_mutex_lock(&mutex);
    if (mysql_query(conn, query)) {
        fprintf(stderr, " [ERROR] 회원가입 실패: %s\n", mysql_error(conn));
        pthread_mutex_unlock(&mutex);
        return 0;
    }
    pthread_mutex_unlock(&mutex);
    printf(" [DEBUG] DB에 회원가입 정보 성공적으로 저장됨.\n");
    return 1;
}


// 이름 (name) 중복 여부를 DB에서 확인
int is_name_taken(const char* name) {
    char query[256];
    char escaped_name[ID_LEN * 2 + 1];
    mysql_real_escape_string(conn, escaped_name, name, strlen(name)); // 이스케이프 처리

    snprintf(query, sizeof(query), " SELECT name FROM users WHERE name = '%s'", escaped_name);
    pthread_mutex_lock(&mutex);
    if (mysql_query(conn, query)) {
        pthread_mutex_unlock(&mutex);
        fprintf(stderr, " 이름 중복 확인 쿼리 실패: %s\n", mysql_error(conn));
        return 1; // 쿼리 실패 시 중복으로 간주 (안전하게)
    }
    MYSQL_RES* res = mysql_store_result(conn);
    pthread_mutex_unlock(&mutex);
    if (!res) {
        fprintf(stderr, " 이름 중복 확인 결과 저장 실패: %s\n", mysql_error(conn));
        return 1; // 결과 가져오기 실패 시 중복으로 간주
    }
    int exists = mysql_num_rows(res) > 0;
    mysql_free_result(res);
    return exists;
}

// ID (username) 중복 여부를 DB에서 확인
int is_id_taken(const char* username) {
    char query[256];
    char escaped_username[ID_LEN * 2 + 1];
    mysql_real_escape_string(conn, escaped_username, username, strlen(username)); // 이스케이프 처리

    snprintf(query, sizeof(query), "SELECT username FROM users WHERE username = '%s'", escaped_username);
    pthread_mutex_lock(&mutex);
    if (mysql_query(conn, query)) {
        pthread_mutex_unlock(&mutex);
        fprintf(stderr, " ID 중복 확인 쿼리 실패: %s\n", mysql_error(conn));
        return 1; // 쿼리 실패 시 중복으로 간주 (안전하게)
    }
    MYSQL_RES* res = mysql_store_result(conn);
    pthread_mutex_unlock(&mutex);
    if (!res) {
        fprintf(stderr, " ID 중복 확인 결과 저장 실패: %s\n", mysql_error(conn));
        return 1; // 결과 가져오기 실패 시 중복으로 간주
    }
    int exists = mysql_num_rows(res) > 0;
    mysql_free_result(res);
    return exists;
}

// ID 유효성 검사 함수 (소문자+숫자, 8자 이상)
int is_valid_id(const char* id) {
    if (strlen(id) < 8) return 0; // 8자 미만

    int has_lower = 0;
    int has_digit = 0;

    for (int i = 0; id[i]; ++i) {
        if (islower(id[i])) has_lower = 1;
        else if (isdigit(id[i])) has_digit = 1;
        else return 0; // 소문자나 숫자가 아닌 문자가 있으면 실패
    }

    return has_lower && has_digit; // 둘 다 있어야 통과
}

// 비밀번호 유효성 검사 함수 (대소문자 포함, 8자 이상)
int is_valid_password(const char* pw) {
    int len = strlen(pw);
    int upper = 0;
    int lower = 0;
    int digit = 0; 

    if (len < 8) return 0; // 8자 미만

    for (int i = 0; i < len; ++i) {
        if (isupper(pw[i])) upper = 1;
        if (islower(pw[i])) lower = 1;
        if (isdigit(pw[i])) digit = 1;
    }
    // 대문자, 소문자, 숫자가 모두 포함되어야 함
    return upper && lower && digit; 
}

// 클라이언트로부터 회원가입 정보를 받고 DB에 저장하는 함수
int signup_user(int sock) {
    char name_input[ID_LEN], username_input[ID_LEN], pw_input[ID_LEN];

    // 이름 입력 및 유효성 검사 (길이 및 중복)
    while (1) {
        send_with_length(sock, " NEW 이름 입력 (2글자 이상) : ");
        if (!receive_message(sock, name_input)) {
            printf(" [DEBUG] signup_user: 이름 입력 중 클라이언트 연결 종료.\n");
            return 0; // 연결 종료 또는 오류
        }
        printf(" [DEBUG] 클라이언트로부터 이름 수신: '%s'\n", name_input); // 디버그 출력

        if (utf8_strlen(name_input) < 2) {
            send_with_length(sock, " 회원가입 실패 : 이름은 2글자 이상이어야 합니다.\n");
            printf(" [DEBUG] 이름 유효성 검사 실패 : 2글자 미만.\n"); // 디버그 출력
            continue; // 다시 이름 입력 요청
        }

        if (is_name_taken(name_input)) { // is_name_taken 내부에서 이스케이프 처리
            send_with_length(sock, " 회원가입 실패 : 이미 사용 중인 이름입니다.\n");
            printf(" [DEBUG] 이름 중복 검사 실패 : 이미 사용 중.\n"); // 디버그 출력
            continue; // 다시 이름 입력 요청
        }
        printf(" [DEBUG] 이름 유효성 검사 및 중복 확인 통과.\n"); // 디버그 출력
        break; // 유효한 이름이므로 루프 탈출
    }

    // ID 입력 및 유효성 검사 (기존 ID 중복 검사 포함)
    while (1) {
        send_with_length(sock, " NEW ID 입력 (소문자+숫자 8자 이상) : ");
        if (!receive_message(sock, username_input)) {
            printf(" [DEBUG] signup_user : ID 입력 중 클라이언트 연결 종료.\n");
            return 0;
        }
        printf(" [DEBUG] 클라이언트로부터 ID 수신: '%s'\n", username_input); // 디버그 출력

        if (!is_valid_id(username_input)) { // ID 유효성 검사
            send_with_length(sock, " 회원가입 실패 : ID는 소문자/숫자 조합 8자 이상이어야 합니다.\n");
            printf(" [DEBUG] ID 유효성 검사 실패: 형식 불일치.\n"); // 디버그 출력
            continue; // 다시 ID 입력 요청
        }

        if (is_id_taken(username_input)) { // is_id_taken 내부에서 이스케이프 처리
            send_with_length(sock, " 회원가입 실패: 이미 사용 중인 ID입니다.\n");
            printf(" [DEBUG] ID 중복 검사 실패: 이미 사용 중.\n"); // 디버그 출력
            continue; // 다시 ID 입력 요청
        }
        printf(" [DEBUG] ID 유효성 검사 및 중복 확인 통과.\n"); // 디버그 출력
        break; // 유효한 ID이므로 루프 탈출
    }

    // 비밀번호 입력 및 유효성 검사
    while (1) {
        send_with_length(sock, " NEW PW 입력 (대소문자, 숫자 포함 8자 이상): ");
        if (!receive_message(sock, pw_input)) {
            printf(" [DEBUG] signup_user: PW 입력 중 클라이언트 연결 종료.\n");
            return 0;
        }
        printf(" [DEBUG] 클라이언트로부터 PW 수신: '%s'\n", pw_input); // 디버그 출력

        if (!is_valid_password(pw_input)) { // 비밀번호 유효성 검사
            send_with_length(sock, " 회원가입 실패 : PW는 대소문자, 숫자를 포함한 8자 이상이어야 합니다.\n");
            printf(" [DEBUG] PW 유효성 검사 실패: 형식 불일치.\n"); // 디버그 출력
            continue; // 다시 PW 입력 요청
        }
        printf(" [DEBUG] PW 유효성 검사 통과.\n"); // 디버그 출력
        break; // 유효한 PW이므로 루프 탈출
    }

    // 모든 유효성 검사 통과 후 DB에 저장
    printf(" [DEBUG] 모든 회원가입 정보 유효성 검사 통과. DB 저장 시도.\n"); // 디버그 출력
    if (!db_signup(name_input, username_input, pw_input)) {
        send_with_length(sock, " 회원가입 실패 : 데이터베이스 저장 오류.\n");
        printf(" [DEBUG] db_signup 실패.\n"); // 디버그 출력
        return 0;
    }
    // 변경: 회원가입 성공 메시지 포맷팅을 클라이언트 파싱에 맞게 수정
    char success_msg[BUF_SIZE];
    snprintf(success_msg, sizeof(success_msg), " 회원가입 성공!\n");
    send_with_length(sock, success_msg);
    printf(" [DEBUG] 회원가입 성공 메시지 클라이언트에 전송 완료.\n"); // 디버그 출력
    return 1;
}


// DB에서 ID (username)/PW를 검증하여 로그인 성공 여부를 반환
// 로그인 함수
int db_login(const char* username, const char* pw) {
    char query[512];
    char escaped_username[ID_LEN * 2 + 1];
    char escaped_pw[ID_LEN * 2 + 1];

    //1. 입력된 비밀번호를 해시로 변환
    char hashed_pw[65];
    hash_256_password(pw, hashed_pw);

    //2. escape 처리 (해시된 pw 사용)
    mysql_real_escape_string(conn, escaped_username, username, strlen(username));
    mysql_real_escape_string(conn, escaped_pw, hashed_pw, strlen(hashed_pw));

    //3. 해시된 비밀번호로 SELECT 쿼리
    snprintf(query, sizeof(query),
        "SELECT * FROM users WHERE username='%s' AND password='%s'",
        escaped_username, escaped_pw);

    printf(" [DEBUG] 로그인 쿼리: %s\n", query);

    pthread_mutex_lock(&mutex);
    int result = mysql_query(conn, query);
    MYSQL_RES* res = mysql_store_result(conn);
    pthread_mutex_unlock(&mutex);

    if (!res) {
        fprintf(stderr, " [ERROR] 로그인 쿼리 실패: %s\n", mysql_error(conn));
        return 0;
    }

    int success = mysql_num_rows(res) > 0;
    mysql_free_result(res);

    if (success) {
        printf(" [DEBUG] 로그인 성공: %s\n", username);
    } else {
        printf(" [DEBUG] 로그인 실패: %s\n", username);
    }

    return success;
}
// DB에서 사용자 이름을 가져오는 함수
int db_get_name(const char* username, char* out_name_buf) {
    char query[256];
    char escaped_username[ID_LEN * 2 + 1];
    mysql_real_escape_string(conn, escaped_username, username, strlen(username)); // 이스케이프 처리

    snprintf(query, sizeof(query), "SELECT name FROM users WHERE username='%s'", escaped_username);
    pthread_mutex_lock(&mutex);
    if (mysql_query(conn, query)) {
        fprintf(stderr, " 이름 조회 쿼리 실패 : %s\n", mysql_error(conn));
        pthread_mutex_unlock(&mutex);
        return 0;
    }
    MYSQL_RES* res = mysql_store_result(conn);
    pthread_mutex_unlock(&mutex);

    if (!res) {
        fprintf(stderr, " 이름 조회 결과 저장 실패 : %s\n", mysql_error(conn));
        return 0;
    }
    MYSQL_ROW row = mysql_fetch_row(res);
    if (row && row[0]) {
        strncpy(out_name_buf, row[0], ID_LEN - 1);
        out_name_buf[ID_LEN - 1] = '\0';
        mysql_free_result(res);
        return 1;
    }
    mysql_free_result(res);
    return 0;
}

// 클라이언트로부터 로그인 정보(ID/PW)를 입력받아 로그인 처리
int login_user(int sock, char* out_username, char* out_name) {
    char pw_input[ID_LEN];

    send_with_length(sock, " ID : ");
    if (!receive_message(sock, out_username)) return 0;

    send_with_length(sock, " PW : ");
    if (!receive_message(sock, pw_input)) return 0;

    // db_login 함수 내에서 이미 이스케이프 처리하므로 여기서는 필요 없음
    if (!db_login(out_username, pw_input)) {
        send_with_length(sock, " 로그인 실패\n");
        return 0;
    }
    
    // 로그인 성공 시 사용자 이름(닉네임)도 가져와서 저장
    if (!db_get_name(out_username, out_name)) {
        fprintf(stderr, " 사용자 이름 가져오기 실패 : %s\n", out_username);
        send_with_length(sock, " 로그인 성공했지만, 사용자 정보를 불러올 수 없습니다.\n");
        return 0;
    }

    // 수정: 로그인 성공 메시지에 사용자 정보 포함
    char success_msg[BUF_SIZE];
    snprintf(success_msg, sizeof(success_msg), " 로그인 성공!%s:%s\n", out_username, out_name);
    send_with_length(sock, success_msg);
    return 1;
}

// 로그인한 사용자가 관리자 권한이 있는지 DB에서 조회
int db_is_admin(const char* username) {
    char query[256];
    char escaped_username[ID_LEN * 2 + 1];
    mysql_real_escape_string(conn, escaped_username, username, strlen(username)); // 이스케이프 처리

    snprintf(query, sizeof(query), " SELECT is_admin FROM users WHERE username='%s'", escaped_username);
    pthread_mutex_lock(&mutex);
    if (mysql_query(conn, query)) {
        fprintf(stderr, " 관리자 확인 쿼리 실패 : %s\n", mysql_error(conn));
        pthread_mutex_unlock(&mutex);
        return 0;
    }
    MYSQL_RES* res = mysql_store_result(conn);
    pthread_mutex_unlock(&mutex);

    if (!res) {
        fprintf(stderr, " 관리자 확인 결과 저장 실패 : %s\n", mysql_error(conn));
        return 0;
    }
    MYSQL_ROW row = mysql_fetch_row(res);
    int admin = (row && atoi(row[0]) == 1);
    mysql_free_result(res);
    return admin;
}

// 소켓 번호로 clients[] 배열에서 해당 클라이언트 포인터 검색
Client* find_client_by_sock(int sock) {
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < MAX_CLIENTS; ++i) {
        if (clients[i] != NULL && clients[i]->sock == sock) {
            pthread_mutex_unlock(&mutex);
            return clients[i];
        }
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// ID (username)로 clients[] 배열에서 해당 클라이언트 포인터 검색
Client* find_client_by_id(const char* username) {
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < MAX_CLIENTS; ++i) {
        if (clients[i] != NULL && strcmp(clients[i]->username, username) == 0) {
            pthread_mutex_unlock(&mutex);
            return clients[i];
        }
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

//이름(name)로 clients[] 배열에서 해당 클라이언트 포인터 검색
Client* find_client_by_name(const char* name) {
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < MAX_CLIENTS; ++i) {
        if (clients[i] != NULL && strcmp(clients[i]->name, name) == 0) {
            pthread_mutex_unlock(&mutex);
            return clients[i];
        }
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// 관리자가 대상 닉네임의 클라이언트를 강제로 접속 종료시키는 함수
void admin_kick(int sender_sock, const char* target_name) {
    Client* target_cli = find_client_by_name(target_name);
    if (target_cli) {
        send_with_length(target_cli->sock, " [서버] 관리자에 의해 추방되었습니다.\n");
        // 해당 소켓을 닫으면 receive_message에서 0을 반환하여 스레드 종료 유도
        close(target_cli->sock);
    } else {
        send_with_length(sender_sock, " [서버] 해당 ID의 사용자를 찾을 수 없습니다.\n");
    }
}

// 관리자가 대상 닉네임의 클라이언트를 채팅 금지 상태로 만드는 함수
void admin_mute(int sender_sock, const char* target_name) {
    Client* target_cli = find_client_by_name(target_name);
    if (target_cli) {
        pthread_mutex_lock(&mutex);
        target_cli->is_muted = 1;
        pthread_mutex_unlock(&mutex);
        send_with_length(target_cli->sock, " [서버] 음소거 되었습니다. 채팅할 수 없습니다.\n");
        send_with_length(sender_sock, " [서버] 사용자 음소거 완료.\n");
    } else {
        send_with_length(sender_sock, " [서버] 해당 ID의 사용자를 찾을 수 없습니다.\n");
    }
}

// 헬프 메시지를 클라이언트에게 전송하는 함수
void help_message(Client* cli) {
    char help_msg[BUF_SIZE * 2]; // 헬프 메시지는 길 수 있으므로 충분히 큰 버퍼를 할당

    // 기본 명령어 목록
    snprintf(help_msg, sizeof(help_msg), 
             "[서버] 사용 가능한 명령어:\n"
             "  /exit               : 전체 채팅방으로 이동합니다.\n"
             "  /w [대상ID] [메시지] : 특정 사용자에게 귓속말을 보냅니다.\n"
             "  /c [방이름]           : 방을 생성합니다.\n"
             "  /j [방이름]           : 방에 들어갑니다.\n"
             "  /l                   : 모든 방을 띄웁니다.\n"
             );
    
    // 관리자 명령어는 관리자에게만 표시
    if (cli->is_admin) {
        strncat(help_msg, 
                "  /kick [대상ID]       : 특정 사용자를 채팅방에서 강제로 내보냅니다.\n"
                "  /mute [대상ID]       : 특정 사용자의 채팅을 일시적으로 금지합니다.\n"
                "  /announce [메시지]   : 모든 접속자에게 공지 메시지를 보냅니다.\n",
                sizeof(help_msg) - strlen(help_msg) - 1);
    }
    // 공통 명령어 (항상 표시)
    strncat(help_msg, "  /help                : 모든 명령어 목록을 표시합니다.\n", sizeof(help_msg) - strlen(help_msg) - 1);

    // 클라이언트에게 헬프 메시지 전송
    send_with_length(cli->sock, help_msg);
}

// 관리자가 전체 클라이언트에게 공지 메시지를 전송하는 함수
void admin_announce(const char* msg) {
    pthread_mutex_lock(&mutex);
    char full_msg[BUF_SIZE];
    snprintf(full_msg, sizeof(full_msg), "[공지] %s\n", msg);
    for (int i = 0; i < MAX_CLIENTS; ++i) {
        if (clients[i] != NULL) {
            send_with_length(clients[i]->sock, full_msg);
        }
    }
    pthread_mutex_unlock(&mutex);
}

// 클라이언트의 ID (username)를 변경하고 DB 반영
// 클라이언트의 ID (username)를 변경하고 DB 반영
int change_id(int client_sock, const char* old_username, char* out_new_username) {
    char new_username[ID_LEN];

    send_with_length(client_sock, " 새 ID 입력 (소문자+숫자 8자 이상) : ");
    if (!receive_message(client_sock, new_username)) {
        printf(" [DEBUG] change_id : 클라이언트 응답 없음\n");
        return 0;
    }

    printf(" [DEBUG] change_id : 입력받은 새 ID = '%s', 길이 = %d\n", new_username, (int)strlen(new_username));

    if (strlen(new_username) == 0) {
        send_with_length(client_sock, " ID가 비어있습니다.\n");
        printf(" [DEBUG] change_id : ID가 비어있음\n");
        return 0;
    }

    // ID 유효성 검사 추가
    if (!is_valid_id(new_username)) {
        send_with_length(client_sock, " ID 변경 실패: ID는 소문자/숫자 조합 8자 이상이어야 합니다.\n");
        printf(" [DEBUG] change_id : ID 유효성 검사 실패 = '%s'\n", new_username);
        return 0;
    }
    printf(" [DEBUG] change_id : ID 유효성 검사 통과\n");

    if (is_id_taken(new_username)) { // is_id_taken 내부에서 이스케이프 처리
        send_with_length(client_sock, " 이미 사용 중인 ID입니다.\n");
        printf(" [DEBUG] change_id : ID 중복 = '%s'\n", new_username);
        return 0;
    }
    printf(" [DEBUG] change_id : ID 중복 검사 통과\n");

    char query[256];
    char escaped_new_username[ID_LEN * 2 + 1];
    char escaped_old_username[ID_LEN * 2 + 1];

    mysql_real_escape_string(conn, escaped_new_username, new_username, strlen(new_username)); // 이스케이프 처리
    mysql_real_escape_string(conn, escaped_old_username, old_username, strlen(old_username)); // 이스케이프 처리

    snprintf(query, sizeof(query), " UPDATE users SET username='%s' WHERE username='%s'", escaped_new_username, escaped_old_username);
    printf(" [DEBUG] change_id : 실행할 쿼리 = '%s'\n", query);

    pthread_mutex_lock(&mutex);
    int result = mysql_query(conn, query);
    pthread_mutex_unlock(&mutex);

    if (result == 0) {
        send_with_length(client_sock, " ID 변경 완료\n");
        printf(" [DEBUG] change_id : ID 변경 성공: %s -> %s\n", old_username, new_username);
        strncpy(out_new_username, new_username, ID_LEN - 1);
        out_new_username[ID_LEN - 1] = '\0';
        return 1;
    } else {
        send_with_length(client_sock, " ID 변경 실패\n");
        printf(" [DEBUG] change_id : ID 변경 실패 : %s\n", mysql_error(conn));
        return 0;
    }
}

// 클라이언트의 비밀번호를 변경하고 DB 반영 (Prepared Statement 적용)
// 성공 시 1 반환, 실패 시 0 반환
int change_pw(int client_sock, const char* username) {
    char new_pw[ID_LEN];          // 사용자로부터 받은 새 비밀번호
    char hashed_pw[65];           // 해시된 비밀번호 (SHA-256은 64자 + 널)
    MYSQL_STMT *stmt;
    MYSQL_BIND bind[2];

    send_with_length(client_sock, " 새 비밀번호를 입력하세요 (8자 이상, 대소문자+숫자 포함): > ");
    if (!receive_message(client_sock, new_pw)) {
        printf(" [서버] PW 변경 - 클라이언트 응답 없음 (소켓 %d)\n", client_sock);
        return 0;
    }

    if (!is_valid_password(new_pw)) {
        send_with_length(client_sock, " [서버] 비밀번호는 8자 이상이며, 대소문자와 숫자를 포함해야 합니다.\n");
        return 0;
    }

    printf(" [서버] PW 변경 시도 : 사용자 %s\n", username);

    // 새 비밀번호 해싱
    hash_256_password(new_pw, hashed_pw);
    printf(" [DEBUG] 해시된 비밀번호: %s\n", hashed_pw); // 선택적 디버깅

    pthread_mutex_lock(&mutex);

    stmt = mysql_stmt_init(conn);
    if (!stmt) {
        fprintf(stderr, " [DB] mysql_stmt_init() failed : %s\n", mysql_error(conn));
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    const char *query = "UPDATE users SET password = ? WHERE username = ?";
    if (mysql_stmt_prepare(stmt, query, strlen(query))) {
        fprintf(stderr, " [DB] mysql_stmt_prepare() failed : %s\n", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    memset(bind, 0, sizeof(bind));

    unsigned long pw_len = strlen(hashed_pw);
    unsigned long user_len = strlen(username);

    bind[0].buffer_type = MYSQL_TYPE_STRING;
    bind[0].buffer = (char*)hashed_pw;
    bind[0].length = &pw_len;

    bind[1].buffer_type = MYSQL_TYPE_STRING;
    bind[1].buffer = (char*)username;
    bind[1].length = &user_len;

    if (mysql_stmt_bind_param(stmt, bind)) {
        fprintf(stderr, " [DB] mysql_stmt_bind_param() failed : %s\n", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    if (mysql_stmt_execute(stmt)) {
        fprintf(stderr, " [DB] mysql_stmt_execute() failed : %s\n", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    if (mysql_stmt_affected_rows(stmt) > 0) {
        send_with_length(client_sock, " 비밀번호 변경이 완료되었습니다.\n");
        printf(" [서버] PW 변경 성공: %s\n", username);
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 1;
    } else {
        send_with_length(client_sock, " 비밀번호 변경 실패. (사용자를 찾을 수 없거나 동일한 비밀번호)\n");
        printf(" [서버] PW 변경 실패 : %s (영향 받은 행 없음)\n", username);
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 0;
    }
}

// 사용자의 닉네임(name)을 변경하는 함수
int db_change_name(const char* user_id, const char* new_name) {
    MYSQL_STMT *stmt;
    MYSQL_BIND bind[3]; // new_name, user_id
    int result = 0;
    char query[BUF_SIZE];
    unsigned long new_name_len, user_id_len;

    // 새 닉네임 길이 검사 (유효성 검사는 호출하는 곳에서 하는 것이 일반적)
    // 여기서는 단순히 길이만 확인
    if (utf8_strlen(new_name) < 2 || utf8_strlen(new_name) > 10) { // 닉네임 길이 제한 (예: 2~10자)
        printf(" [DB] 닉네임 길이 유효성 검사 실패 : %s\n", new_name);
        return 0; // 유효성 검사 실패
    }

    pthread_mutex_lock(&mutex); // DB 접근 뮤텍스 잠금

    // Prepare statement로 SQL 인젝션 방지
    snprintf(query, BUF_SIZE, " UPDATE users SET name = ? WHERE username = ?");
    stmt = mysql_stmt_init(conn);
    if (!stmt) {
        fprintf(stderr, " [DB] mysql_stmt_init() failed : %s\n", mysql_error(conn));
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    if (mysql_stmt_prepare(stmt, query, strlen(query))) {
        fprintf(stderr, " [DB] mysql_stmt_prepare() failed : %s\n", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    // 바인딩 파라미터 초기화
    memset(bind, 0, sizeof(bind));

    // new_name 바인딩
    bind[0].buffer_type = MYSQL_TYPE_STRING;
    bind[0].buffer = (char*)new_name;
    new_name_len = strlen(new_name);
    bind[0].length = &new_name_len;

    // user_id 바인딩
    bind[1].buffer_type = MYSQL_TYPE_STRING;
    bind[1].buffer = (char*)user_id;
    user_id_len = strlen(user_id);
    bind[1].length = &user_id_len;

    if (mysql_stmt_bind_param(stmt, bind)) {
        fprintf(stderr, " [DB] mysql_stmt_bind_param() failed : %s\n", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    if (mysql_stmt_execute(stmt)) {
        fprintf(stderr, " [DB] mysql_stmt_execute() failed : %s\n", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        pthread_mutex_unlock(&mutex);
        return 0;
    }

    if (mysql_stmt_affected_rows(stmt) > 0) {
        result = 1; // 성공
    } else {
        printf(" [DB] 닉네임 변경 실패 : %s -> %s (영향 받은 행 없음)\n", user_id, new_name);
    }

    mysql_stmt_close(stmt);
    pthread_mutex_unlock(&mutex); // DB 접근 뮤텍스 해제
    return result;
}


// 사용자 ID (username)에 해당하는 레코드를 users 테이블에서 삭제하는 함수
int delete_user(const char* username) {
    char query[256];
    char escaped_username[ID_LEN * 2 + 1];
    mysql_real_escape_string(conn, escaped_username, username, strlen(username)); // 이스케이프 처리

    snprintf(query, sizeof(query), " DELETE FROM users WHERE username = '%s'", escaped_username);

    pthread_mutex_lock(&mutex);
    int result = mysql_query(conn, query);
    pthread_mutex_unlock(&mutex);

    if (result == 0) {
        printf(" [서버] 사용자 삭제 성공 : %s\n", username);
        return 1;
    } else {
        printf(" [서버] 사용자 삭제 실패 : %s\n", mysql_error(conn));
        return 0;
    }
}

void broadcast_to_room(const char* roomname, const char* msg, int except_sock) {
    pthread_mutex_lock(&mutex);
    
    printf(" [DEBUG] broadcast_to_room: 방 = '%s', 메시지 = '%s', 제외소켓 = %d\n", roomname, msg, except_sock);
    
    int sent_count = 0;
    for (int i = 0; i < MAX_CLIENTS; ++i) {
        if (clients[i] && strcmp(clients[i]->room, roomname) == 0 && clients[i]->sock != except_sock) {
            printf(" [DEBUG] 메시지 전송 : %s(%s)에게 전송\n", clients[i]->name, clients[i]->username);
            send_with_length(clients[i]->sock, msg);
            sent_count++;
        }
    }
    
    printf(" [DEBUG] 총 %d명에게 메시지 전송 완료\n", sent_count);
    pthread_mutex_unlock(&mutex);
}



// 귓속말 전송
void whisper(const char *from_name, const char *to_name, const char *body, int sender_sock) {
    char fmt[BUF_SIZE];
    char time_str[16];  // 추가
    Client* target_cli = find_client_by_name(to_name); 

    // 현재 시간 가져오기 (추가)
    get_current_time(time_str);

    if (target_cli == NULL || target_cli->room[0] == '\0') { 
        char not_found_msg[BUF_SIZE];
        snprintf(not_found_msg, sizeof(not_found_msg), " [서버] '%s'님을 찾을 수 없습니다. (현재 접속 중인 사용자가 아니거나 ID가 다릅니다.)\n", to_name);
        send_with_length(sender_sock, not_found_msg);
    } else {
        // 시간 포함으로 수정
        snprintf(fmt, sizeof(fmt), "%s [귓속말] %s → %s: %s\n", time_str, from_name, to_name, body);
        send_with_length(target_cli->sock, fmt);
    }
}

int save_chat_log(const char* sender, const char* room, const char* message, 
                  int is_whisper, const char* recipients) {
    char query[2048];
    char escaped_sender[ID_LEN * 2 + 1];
    char escaped_room[ID_LEN * 2 + 1];
    char escaped_message[BUF_SIZE * 2 + 1];
    char escaped_recipients[300]; // recipients 길이의 2배 + 1

    // SQL 인젝션 방지를 위한 이스케이프 처리
    mysql_real_escape_string(conn, escaped_sender, sender, strlen(sender));
    mysql_real_escape_string(conn, escaped_message, message, strlen(message));
    
    // room과 recipients는 NULL일 수 있음
    if (room) {
        mysql_real_escape_string(conn, escaped_room, room, strlen(room));
    }
    if (recipients) {
        mysql_real_escape_string(conn, escaped_recipients, recipients, strlen(recipients));
    }

    // 쿼리 작성
    if (room && recipients) {
        // 방과 수신자 모두 있는 경우
        snprintf(query, sizeof(query),
            "INSERT INTO chat_logs (sender, room, message, is_whisper, recipients, created_at) "
            "VALUES('%s', '%s', '%s', %d, '%s', NOW())", 
            escaped_sender, escaped_room, escaped_message, is_whisper, escaped_recipients);
    } else if (room) {
        // 방만 있는 경우 (전체 채팅)
        snprintf(query, sizeof(query),
            "INSERT INTO chat_logs (sender, room, message, is_whisper, recipients, created_at) "
            "VALUES('%s', '%s', '%s', %d, NULL, NOW())", 
            escaped_sender, escaped_room, escaped_message, is_whisper);
    } else if (recipients) {
        // 수신자만 있는 경우 (귓속말)
        snprintf(query, sizeof(query),
            "INSERT INTO chat_logs (sender, room, message, is_whisper, recipients, created_at) "
            "VALUES('%s', NULL, '%s', %d, '%s', NOW())", 
            escaped_sender, escaped_message, is_whisper, escaped_recipients);
    } else {
        // 둘 다 없는 경우 (전체 채팅, 방 없음)
        snprintf(query, sizeof(query),
            "INSERT INTO chat_logs (sender, room, message, is_whisper, recipients, created_at) "
            "VALUES('%s', NULL, '%s', %d, NULL, NOW())", 
            escaped_sender, escaped_message, is_whisper);
    }

    pthread_mutex_lock(&mutex);
    int result = mysql_query(conn, query);
    if (result != 0) {
        fprintf(stderr, " 채팅 로그 저장 실패 : %s\n", mysql_error(conn));
        pthread_mutex_unlock(&mutex);
        return 0;
    }
    pthread_mutex_unlock(&mutex);
    
    printf(" [DB LOG] 채팅 저장됨 - 보낸이 : %s, 귓속말 : %s, 수신자 : %s\n", 
           sender, is_whisper ? "YES" : "NO", recipients ? recipients : "ALL");
    return 1;
}

//방 생성 함수
void handle_create_room(Client* cli, const char* roomname) {
    for (int i = 0; i < room_count; i++) {
        if (strcmp(chat_rooms[i].name, roomname) == 0) {
            send_with_length(cli->sock, " [서버] 이미 존재하는 방입니다.\n");
            return;
        }
    }

    if (room_count >= MAX_ROOMS) {
        send_with_length(cli->sock, " [서버] 방 개수 제한 초과입니다.\n");
        return;
    }

    pthread_mutex_lock(&mutex);
    strncpy(chat_rooms[room_count].name, roomname, ID_LEN - 1);
    chat_rooms[room_count].member_count = 0;
    room_count++;
    pthread_mutex_unlock(&mutex);

    send_with_length(cli->sock, " [서버] 방이 생성되었습니다. /j 방이름 으로 입장하세요.\n");
}

//채팅방에 입장하는 함수
void handle_join_room(Client* cli, const char* roomname) {
    int found = 0;
    int room_index = -1;
    
    // 방 찾기
    for (int i = 0; i < room_count; i++) {
        if (strcmp(chat_rooms[i].name, roomname) == 0) {
            found = 1;
            room_index = i;
            break;
        }
    }

    if (!found) {
        send_with_length(cli->sock, " [서버] 존재하지 않는 방입니다.\n");
        return;
    }

    pthread_mutex_lock(&mutex);
    
    // 현재 방 정보 저장
    char previous_room[ID_LEN];
    strncpy(previous_room, cli->room, ID_LEN - 1);
    previous_room[ID_LEN - 1] = '\0';
    
    // 이전 방에서 나가기 (멤버 수 감소)
    if (strlen(previous_room) > 0 && strcmp(previous_room, "ALL") != 0) {
        for (int i = 0; i < room_count; i++) {
            if (strcmp(chat_rooms[i].name, previous_room) == 0) {
                if (chat_rooms[i].member_count > 0) {
                    chat_rooms[i].member_count--;
                }
                break;
            }
        }
    }
    
    // 새 방에 입장
    strncpy(cli->room, roomname, ID_LEN - 1);
    cli->room[ID_LEN - 1] = '\0';
    
    // 새 방의 멤버 수 증가
    chat_rooms[room_index].member_count++;
    
    pthread_mutex_unlock(&mutex);

    // 이전 방에서 나가는 메시지 (ALL이 아닌 경우만)
    if (strlen(previous_room) > 0 && strcmp(previous_room, "ALL") != 0) {
        char leave_msg[BUF_SIZE];
        snprintf(leave_msg, sizeof(leave_msg), "[서버] %s님이 '%s' 방을 나갔습니다.\n", cli->name, previous_room);
        broadcast_to_room(previous_room, leave_msg, cli->sock);
    }
    
    // 새 방에 입장 메시지
    char enter_msg[BUF_SIZE];
    snprintf(enter_msg, sizeof(enter_msg), "[서버] %s님이 '%s' 방에 입장했습니다.\n", cli->name, roomname);
    broadcast_to_room(roomname, enter_msg, cli->sock);

    // 클라이언트에게 성공 메시지
    char success_msg[BUF_SIZE];
    snprintf(success_msg, sizeof(success_msg), "[서버] '%s' 방에 입장했습니다.\n", roomname);
    send_with_length(cli->sock, success_msg);
}

//채팅방 나가기 /exit
int handle_leave_room(Client* cli) {
    pthread_mutex_lock(&mutex);
    
    if (strlen(cli->room) == 0 || strcmp(cli->room, "ALL") == 0) {
        // 전체 채팅방에서 나가기 - 채팅 종료
        cli->room[0] = '\0';  // 방 정보 초기화
        pthread_mutex_unlock(&mutex);
        send_with_length(cli->sock, " [서버] 채팅을 종료하고 전체 채팅으로 돌아갑니다.\n");
        return 1; // 메인 메뉴로 돌아가기
    } else {
        // 특정 방에서 나가기
        char leave_msg[BUF_SIZE];
        snprintf(leave_msg, sizeof(leave_msg), " [서버] %s님이 '%s' 방을 나갔습니다.\n", cli->name, cli->room);
        
        // 현재 방의 멤버 수 감소
        for (int i = 0; i < room_count; i++) {
            if (strcmp(chat_rooms[i].name, cli->room) == 0) {
                if (chat_rooms[i].member_count > 0) {
                    chat_rooms[i].member_count--;
                }
                break;
            }
        }
        
        // 방을 나가는 메시지를 현재 방에 브로드캐스트
        broadcast_to_room(cli->room, leave_msg, cli->sock);
        
        // 전체 채팅방으로 이동
        strncpy(cli->room, "ALL", ID_LEN - 1);
        cli->room[ID_LEN - 1] = '\0';
        pthread_mutex_unlock(&mutex);

        send_with_length(cli->sock, " [서버] 전체 채팅방으로 이동했습니다. 계속 채팅하거나 /exit로 메인 메뉴로 이동하세요.\n");
        return 0; // 채팅 루프 계속
    }
}

void cleanup_client_room(Client* cli) {
    if (cli && strlen(cli->room) > 0 && strcmp(cli->room, "ALL") != 0) {
        pthread_mutex_lock(&mutex);
        for (int i = 0; i < room_count; i++) {
            if (strcmp(chat_rooms[i].name, cli->room) == 0) {
                if (chat_rooms[i].member_count > 0) {
                    chat_rooms[i].member_count--;
                }
                break;
            }
        }
        pthread_mutex_unlock(&mutex);
    }
}

// 현재 방 목록을 클라이언트에게 전송하는 함수
void handle_list_rooms(Client* cli) {
    char msg[BUF_SIZE] = " [서버] 현재 방 목록 : \n";
    for (int i = 0; i < room_count; i++) {
        char line[100];
        snprintf(line, sizeof(line), "  - %s (%d명)\n", chat_rooms[i].name, chat_rooms[i].member_count);
        strncat(msg, line, sizeof(msg) - strlen(msg) - 1);
    }
    send_with_length(cli->sock, msg);
}


// 클라이언트 처리 및 채팅 세션 통합 함수
void* chat_session(void *arg) {
   Client *cli = (Client*)arg;
   int sock = cli->sock;
   char input_buf[BUF_SIZE]; // 사용자 입력 버퍼
   char msg_buf[BUF_SIZE];   // 채팅 메시지 버퍼

   printf(" [서버] 새로운 클라이언트 연결 : %s (소켓 %d)\n", cli->username, sock);

   // 전체 세션을 감싸는 루프 (로그아웃 시 다시 로그인 메뉴로)
   while (1) {
       // 클라이언트 정보 초기화 (로그아웃 후 재로그인을 위해)
       pthread_mutex_lock(&mutex);
       cli->is_admin = 0;
       cli->is_muted = 0;
       cli->room[0] = '\0';
       // username과 name을 임시값으로 재설정
       snprintf(cli->username, ID_LEN - 1, " Guest%d", sock);
       cli->username[ID_LEN - 1] = '\0';
       snprintf(cli->name, ID_LEN - 1, " 게스트%d", sock);
       cli->name[ID_LEN - 1] = '\0';
       pthread_mutex_unlock(&mutex);

       // ------------------- 1. 초기 로그인/회원가입 메뉴 -------------------
       int logged_in = 0;
       while (!logged_in) {
           send_with_length(sock, "--------------------------------------------------------------------\n\n");
           send_with_length(sock, "                    1.회원가입 2.로그인 3. 종료 \n\n");
           send_with_length(sock, "--------------------------------------------------------------------\n\n");
           send_with_length(sock, " 입력 >> ");
           if (!receive_message(sock, input_buf)) {
               printf(" [서버] 클라이언트(%s) 연결 종료 (초기 메뉴).\n", cli->username);
               goto cleanup; // 연결 끊김
           }
       
           if (strcmp(input_buf, "1") == 0) { // 회원가입
               if (!signup_user(sock)) {
                   // signup_user 내부에서 이미 메시지를 보냄
               }
               continue;
           } else if (strcmp(input_buf, "2") == 0) { // 로그인
               if (!login_user(sock, cli->username, cli->name)) {
                   continue;
               }
               // 로그인 성공 시, cli 구조체에 ID와 관리자 권한 업데이트
               cli->is_admin = db_is_admin(cli->username);
               if (cli->is_admin) {
                   send_with_length(sock, " 관리자님 어서오세요!\n");
               }
               logged_in = 1; // 로그인 성공
               printf(" [서버] 클라이언트 %s(%s) 로그인 성공 (소켓 %d)\n", cli->name, cli->username, sock);
               break; // 로그인 성공, 다음 메뉴로 이동
           } else if (strcmp(input_buf, "3") == 0) { // 종료
               send_with_length(sock, " 정말로 종료하시겠습니까? (y/n) > ");
               
               if (!receive_message(sock, input_buf)) {
                   printf(" [서버] 클라이언트(%s) 연결 종료 (종료 확인 중).\n", cli->username);
                   goto cleanup;
               }
               
               if (strcmp(input_buf, "y") == 0 || strcmp(input_buf, "Y") == 0) {
                   send_with_length(sock, " 서버 연결을 종료합니다.\n");
                   sleep(1);
                   shutdown(sock, SHUT_WR);
                   goto cleanup;
               } else if (strcmp(input_buf, "n") == 0 || strcmp(input_buf, "N") == 0) {
                   send_with_length(sock, " 서버 연결 종료를 취소합니다.\n");
                   continue;
               } else {
                   send_with_length(sock, " 잘못된 입력입니다. 종료를 취소합니다.\n");
                   continue;
               }
           } else {
               send_with_length(sock, " 잘못된 입력입니다. 다시 시도하세요.\n");
           }
       }

       // ------------------- 2. 메인 메뉴 루프 -------------------
       int logout_requested = 0;
       while (!logout_requested) {
           send_with_length(sock, "\033[2J\033[1;1H");
           send_with_length(sock, "--------------------------------------------------------------------\n ");
           send_with_length(sock, "                          [메인 메뉴]\n");
           send_with_length(sock, "--------------------------------------------------------------------\n\n");
           send_with_length(sock, "             1. 개인정보 변경 2. 채팅 시작 3. 로그아웃\n\n");
           send_with_length(sock, "--------------------------------------------------------------------\n\n");
           send_with_length(sock, " 입력 >> ");
           if (!receive_message(sock, input_buf)) {
               printf(" [서버] 클라이언트(%s) 연결 종료 (메인 메뉴).\n", cli->username);
               goto cleanup;
           }

           if (strcmp(input_buf, "1") == 0) { // 개인정보 변경 서브 메뉴
               send_with_length(sock, "\033[2J\033[1;1H"); // 화면 클리어 추가
               while (1) {
                   send_with_length(sock, "--------------------------------------------------------------------\n ");
                   send_with_length(sock, "                         [개인정보 변경]\n");
                   send_with_length(sock, "--------------------------------------------------------------------\n\n");
                   send_with_length(sock, " 1. ID 변경 2. 비밀번호 변경 3. 닉네임 변경 4. 회원탈퇴 5. 돌아가기\n\n");
                   send_with_length(sock, "--------------------------------------------------------------------\n\n");
                   send_with_length(sock, " 입력 >> ");
                   if (!receive_message(sock, input_buf)) {
                       printf(" [서버] 클라이언트(%s) 연결 종료 (개인정보 변경 서브 메뉴).\n", cli->username);
                       goto cleanup;
                   }
               
                   if (strcmp(input_buf, "1") == 0) { // ID 변경
                       send_with_length(sock, " ID 변경을 하시겠습니까? (y/n)\n> ");
                       usleep(50000);
                       if (!receive_message(sock, input_buf)) {
                           printf(" [서버] 클라이언트(%s) 연결 종료 (ID 변경 확인).\n", cli->username);
                           goto cleanup;
                       }

                       if (strcmp(input_buf, "Y") == 0 || strcmp(input_buf, "y") == 0) {
                           char new_username[ID_LEN];
                           if (change_id(sock, cli->username, new_username)) {
                               pthread_mutex_lock(&mutex);
                               strncpy(cli->username, new_username, ID_LEN - 1);
                               cli->username[ID_LEN - 1] = '\0';
                               pthread_mutex_unlock(&mutex);
                               send_with_length(sock, " ID가 변경되었습니다.\n");
                           }
                       } else if (strcmp(input_buf, "N") == 0 || strcmp(input_buf, "n") == 0) {
                           send_with_length(sock, " ID 변경을 취소합니다.\n");
                       } else {
                           send_with_length(sock, " 잘못된 입력입니다. Y 또는 N을 입력해주세요.\n");
                       }

                   } else if (strcmp(input_buf, "2") == 0) { // 비밀번호 변경
                       send_with_length(sock, " PW 변경을 하시겠습니까? (y/n)\n> ");
                       usleep(50000);
                       if (!receive_message(sock, input_buf)) {
                           printf(" [서버] 클라이언트(%s) 연결 종료 (PW 변경 확인).\n", cli->username);
                           goto cleanup;
                       }

                       if (strcmp(input_buf, "Y") == 0 || strcmp(input_buf, "y") == 0) {
                           if (change_pw(sock, cli->username)) {
                               send_with_length(sock, " 비밀번호가 변경되었습니다.\n");
                           }
                       } else if (strcmp(input_buf, "N") == 0 || strcmp(input_buf, "n") == 0) {
                           send_with_length(sock, " PW 변경을 취소합니다.\n");
                       } else {
                           send_with_length(sock, " 잘못된 입력입니다. Y 또는 N을 입력해주세요.\n");
                       }

                   } else if (strcmp(input_buf, "3") == 0) { // 닉네임 변경
                       send_with_length(sock, " 닉네임 변경을 하시겠습니까? (y/n)\n> ");
                       usleep(50000);
                       if (!receive_message(sock, input_buf)) {
                           printf(" [서버] 클라이언트(%s) 연결 종료 (닉네임 변경 확인).\n", cli->username);
                           goto cleanup;
                       }

                       if (strcmp(input_buf, "Y") == 0 || strcmp(input_buf, "y") == 0) {
                           char new_name[ID_LEN];
                           char confirm_msg[BUF_SIZE];
                           send_with_length(sock, " 새 닉네임을 입력하세요 (2~10자, 한글/영어/숫자) : ");
                           if (!receive_message(sock, new_name)) {
                               printf(" [서버] 클라이언트(%s) 연결 종료 (닉네임 변경 입력).\n", cli->username);
                               goto cleanup;
                           }
                       
                           if (utf8_strlen(new_name) < 2 || utf8_strlen(new_name) > 10) {
                               send_with_length(sock, " [서버] 닉네임은 2자 이상 10자 이하로 입력해주세요.\n");
                           }
                           else if (is_name_taken(new_name)) {
                               send_with_length(sock, " [서버] 이미 사용 중인 닉네임입니다.\n");
                           }
                           else if (db_change_name(cli->username, new_name)) {
                               pthread_mutex_lock(&mutex);
                               strncpy(cli->name, new_name, ID_LEN - 1);
                               cli->name[ID_LEN - 1] = '\0';
                               pthread_mutex_unlock(&mutex);
                               snprintf(confirm_msg, BUF_SIZE, " 닉네임이 '%s'(으)로 변경되었습니다.\n", cli->name);
                               send_with_length(sock, confirm_msg);
                           } else {
                               send_with_length(sock, " 닉네임 변경 실패. 다시 시도해주세요.\n");
                           }
                       } else if (strcmp(input_buf, "N") == 0 || strcmp(input_buf, "n") == 0) {
                           send_with_length(sock, " 닉네임 변경을 취소합니다.\n");
                       } else {
                           send_with_length(sock, " 잘못된 입력입니다. Y 또는 N을 입력해주세요.\n");
                       }

                   } else if (strcmp(input_buf, "4") == 0) { // 회원 탈퇴
                       send_with_length(sock, " 정말로 회원 탈퇴하시겠습니까? (y/n) > ");
                       if (!receive_message(sock, input_buf)) {
                           printf(" [서버] 클라이언트(%s) 연결 종료 (회원탈퇴 확인).\n", cli->username);
                           goto cleanup;
                       }
                       if (strcmp(input_buf, "y") == 0 || strcmp(input_buf, "Y") == 0) {
                           if (delete_user(cli->username)) {
                               send_with_length(sock, " 회원 탈퇴가 완료되었습니다. 서버 연결을 종료합니다.\n");
                               sleep(1);
                               shutdown(sock, SHUT_WR);
                               goto cleanup;
                           } else {
                               send_with_length(sock, " 회원 탈퇴 실패.\n");
                           }
                       } else {
                           send_with_length(sock, " 회원 탈퇴를 취소합니다.\n");
                       }
                   } else if (strcmp(input_buf, "5") == 0) { // 돌아가기
                       send_with_length(sock, " 메인 메뉴로 돌아갑니다.\n");
                       break;
                   } else {
                       send_with_length(sock, " 잘못된 입력입니다. 다시 선택해주세요.\n");
                   }
               }
           } else if (strcmp(input_buf, "2") == 0) { // 채팅 시작
               send_with_length(sock, "\033[2J\033[1;1H"); // 화면 클리어 추가
               pthread_mutex_lock(&mutex);
               strncpy(cli->room, "ALL", ID_LEN - 1);  // 전체방 자동 진입
               cli->room[ID_LEN - 1] = '\0';           // 널 종료 확실히
               pthread_mutex_unlock(&mutex);

               send_with_length(cli->sock, " 채팅을 시작합니다. /help 입력시 명령어 호출\n");

               char enter_msg[BUF_SIZE];
               snprintf(enter_msg, sizeof(enter_msg), " [서버] %s님이 채팅방에 입장했습니다.\n", cli->name);
               broadcast_to_room("ALL", enter_msg, sock);

               printf(" [DEBUG] 채팅 시작: %s(%s)이 '%s' 방에 입장\n", cli->name, cli->username, cli->room);

               // 채팅 루프
               while (1) {
                   if (!receive_message(sock, msg_buf)) {
                       printf(" [서버] 클라이언트(%s) 연결 종료 (채팅 중).\n", cli->username);
                       goto cleanup;
                   }

                       // 음소거 체크
                   if (cli->is_muted) {
                       send_with_length(sock, " [서버] 음소거 상태에서는 채팅할 수 없습니다.\n");
                       continue;
                   }

                   printf(" [DEBUG] 메시지 수신: %s(%s) in '%s': %s\n", cli->name, cli->username, cli->room, msg_buf);

                   // === /exit 처리 ===
                   if (strcmp(msg_buf, "/exit") == 0) {
                       pthread_mutex_lock(&mutex);

                       if (strlen(cli->room) == 0 || strcmp(cli->room, "ALL") == 0) {
                           // 전체 채팅방에 있는 경우
                           pthread_mutex_unlock(&mutex);
                           send_with_length(sock, " [서버] 채팅을 종료하고 메인 메뉴로 돌아갑니다.\n");
                           break; // 채팅 루프 종료하고 메인 메뉴로
                       } else {
                           // 특정 방에서 나가기
                           char current_room[ID_LEN];
                           strncpy(current_room, cli->room, ID_LEN - 1);
                           current_room[ID_LEN - 1] = '\0';

                           char leave_msg[BUF_SIZE];
                           snprintf(leave_msg, sizeof(leave_msg), " [서버] %s님이 '%s' 방을 나갔습니다.\n", cli->name, current_room);

                           // 현재 방의 멤버 수 감소 (뮤텍스 안에서 처리)
                           for (int i = 0; i < room_count; i++) {
                               if (strcmp(chat_rooms[i].name, current_room) == 0) {
                                   if (chat_rooms[i].member_count > 0) {
                                       chat_rooms[i].member_count--;
                                   }
                                   break;
                               }
                           }

                           // 전체 채팅방으로 이동 (뮤텍스 안에서 처리)
                           strncpy(cli->room, "ALL", ID_LEN - 1);
                           cli->room[ID_LEN - 1] = '\0';

                           pthread_mutex_unlock(&mutex);

                           // 방을 나가는 메시지를 이전 방에 브로드캐스트
                           broadcast_to_room(current_room, leave_msg, sock);

                           // 전체 채팅방 입장 메시지
                           char enter_all_msg[BUF_SIZE];
                           snprintf(enter_all_msg, sizeof(enter_all_msg), " [서버] %s님이 전체 채팅방에 입장했습니다.\n", cli->name);
                           broadcast_to_room("ALL", enter_all_msg, sock);

                           // 클라이언트에게 성공 메시지 전송
                           send_with_length(sock, " [서버] 방을 나가서 전체 채팅방으로 이동했습니다.\n");
                       }
                       continue; // 채팅 루프 계속
                   }
                   // === 기타 명령 ===
                   else if (strncmp(msg_buf, "/c ", 3) == 0) {
                       handle_create_room(cli, msg_buf + 3);
                   } else if (strncmp(msg_buf, "/j ", 3) == 0) {
                       handle_join_room(cli, msg_buf + 3);
                   }else if (strcmp(msg_buf, "/help") == 0){
                       help_message(cli);
                   }else if (strcmp(msg_buf, "/l") == 0) {
                       handle_list_rooms(cli);
                   } else if (strncmp(msg_buf, "/w ", 3) == 0) {
                       char *p = msg_buf + 3;
                       char target_username[ID_LEN], body[BUF_SIZE];
                       if (sscanf(p, "%49s %1023[^\n]", target_username, body) >= 2) {
                           target_username[strcspn(target_username, "\n ")] = '\0';
                           body[strcspn(body, "\n")] = '\0';
                           whisper(cli->username, target_username, body, sock);
                           save_chat_log(cli->username, NULL, body, 1, target_username);
                       } else {
                           send_with_length(sock, " 사용법 : /w 대상ID 메시지\n");
                       }
                   } else if (strncmp(msg_buf, "/kick ", 6) == 0) {
                       if (cli->is_admin) {
                           char target_username_kick[ID_LEN];
                           strncpy(target_username_kick, msg_buf + 6, ID_LEN - 1);
                           target_username_kick[ID_LEN - 1] = '\0';
                           target_username_kick[strcspn(target_username_kick, "\n ")] = '\0';
                           admin_kick(sock, target_username_kick);
                           save_chat_log(cli->username, NULL, msg_buf, 0, NULL);
                       } else {
                           send_with_length(sock, " [서버] 관리자만 사용 가능한 명령어입니다.\n");
                       }
                   } else if (strncmp(msg_buf, "/mute ", 6) == 0) {
                       if (cli->is_admin) {
                           char target_username_mute[ID_LEN];
                           strncpy(target_username_mute, msg_buf + 6, ID_LEN - 1);
                           target_username_mute[ID_LEN - 1] = '\0';
                           target_username_mute[strcspn(target_username_mute, "\n ")] = '\0';
                           admin_mute(sock, target_username_mute);
                           save_chat_log(cli->username, NULL, msg_buf, 0, NULL);
                       } else {
                           send_with_length(sock, " [서버] 관리자만 사용 가능한 명령어입니다.\n");
                       }
                   } else if (strncmp(msg_buf, "/announce ", 10) == 0) {
                       if (cli->is_admin) {
                           admin_announce(msg_buf + 10);
                           save_chat_log(cli->username, NULL, msg_buf, 0, NULL);
                       } else {
                           send_with_length(sock, " [서버] 관리자만 사용 가능한 명령어입니다.\n");
                       }
                   } else {
                       // 일반 메시지
                       if (strlen(cli->room) == 0) {
                           send_with_length(sock, "      [서버] 먼저 채팅방에 입장하세요. /r로 목록 확인 후 /j 방이름 으로 입장.\n");
                       } else {
                           char time_str[16];  // 추가
                           get_current_time(time_str);  // 추가
                           char formatted_msg[BUF_SIZE]; 
                           // 시간 포함으로 수정
                           snprintf(formatted_msg, sizeof(formatted_msg), "%s [%s] %s\n",
                                       time_str, cli->name, msg_buf);
                           broadcast_to_room(cli->room, formatted_msg, sock);
                           save_chat_log(cli->username, cli->room, msg_buf, 0, NULL);  // 추가
                       }
                   }
               }
               char exit_msg[BUF_SIZE];
               snprintf(exit_msg, sizeof(exit_msg), " [서버] %s님이 채팅을 종료했습니다.\n", cli->name);
               broadcast_to_room("ALL", exit_msg, sock);
           } else if (strcmp(input_buf, "3") == 0) { // 로그아웃
               send_with_length(sock, "\033[2J\033[1;1H"); // 화면 클리어 추가
               send_with_length(sock, " 로그아웃을 하시겠습니까? (y/n)\n> ");
               if (!receive_message(sock, input_buf)) {
                   printf(" [서버] 클라이언트(%s) 연결 종료 (로그아웃 확인).\n", cli->username);
                   goto cleanup;
               }
               
               if (strcmp(input_buf, "Y") == 0 || strcmp(input_buf, "y") == 0) {
                   send_with_length(sock, " 로그아웃되었습니다. 초기 메뉴로 돌아갑니다.\n");
                   printf(" [서버] 클라이언트 %s(%s) 로그아웃 (소켓 %d)\n", cli->name, cli->username, sock);
                   logout_requested = 1;
                   break;
               } else if (strcmp(input_buf, "N") == 0 || strcmp(input_buf, "n") == 0) {
                   send_with_length(sock, " 로그아웃을 취소합니다.\n");
               } else {
                   send_with_length(sock, " 잘못된 입력입니다. Y 또는 N을 입력해주세요.\n");
               }
           } else {
               send_with_length(sock, " 잘못된 입력입니다. 다시 선택해주세요.\n");
           }
       } // end of 메인 메뉴 루프
       
       // 로그아웃된 경우 다시 초기 메뉴로 돌아가기
       if (logout_requested) {
           continue; // 전체 세션 루프의 처음으로 돌아가서 다시 로그인 메뉴 표시
       }
   } // end of 전체 세션 루프

cleanup:
   cleanup_client_room(cli);

   printf(" [서버] 클라이언트 연결 종료: %s (%s, 소켓 %d)\n", cli->name, cli->username, sock);
   close(sock);

   pthread_mutex_lock(&mutex);
   for (int i = 0; i < MAX_CLIENTS; ++i) {
       if (clients[i] == cli) {
           clients[i] = NULL;
           client_count--;
           break;
       }
   }
   pthread_mutex_unlock(&mutex);
   free(cli);
   return NULL;
}

int main(int argc, char* argv[]) {
    int serv_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;

    if (argc != 2) {
        printf("Usage: %s <PORT>\n", argv[0]);
        exit(1);
    }

    // MySQL 라이브러리 초기화 및 연결
    mysql_library_init(0, NULL, NULL);
    conn = mysql_init(NULL);
    if (!conn) {
        fprintf(stderr, "MySQL init error\n");
        exit(1);
    }

    if (!mysql_real_connect(conn, DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT, NULL, 0)) {
        fprintf(stderr, "MySQL connect error: %s\n", mysql_error(conn));
        exit(1);
    }
    printf("[MySQL] DB 연결 성공\n");

    // 서버 소켓 생성
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1) error_handling("socket() error");

    // TIME_WAIT 상태를 피하기 위한 SO_REUSEADDR 옵션 설정
    int opt = 1;
    setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    // 소켓 바인딩
    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    
    // 연결 요청 대기
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    
    printf("[서버] 포트 %d에서 대기 중...\n", atoi(argv[1]));

    // 클라이언트 포인터 배열 초기화
    for(int i = 0; i < MAX_CLIENTS; ++i) {
        clients[i] = NULL;
    }

    while (1) {
        clnt_adr_sz = sizeof(clnt_adr);
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if (clnt_sock == -1) {
            perror("accept error");
            continue;
        }

        pthread_mutex_lock(&mutex);
        // 비어있는 clients 배열 슬롯 찾기
        int idx = -1;
        for (int i = 0; i < MAX_CLIENTS; ++i) {
            if (clients[i] == NULL) {
                idx = i;
                break;
            }
        }

        if (idx == -1) { // 최대 접속 수 초과
            send_with_length(clnt_sock, "서버에 접속자가 많아 연결할 수 없습니다.\n");
            close(clnt_sock);
            pthread_mutex_unlock(&mutex);
            printf("[서버] 최대 접속 수 도달, 새 연결 거부\n");
            continue;
        }

        // 새로운 Client 구조체 동적 할당
        Client* cli = malloc(sizeof(Client));
        if (!cli) {
            perror("malloc error for Client");
            close(clnt_sock);
            pthread_mutex_unlock(&mutex);
            continue;
        }

        cli->sock = clnt_sock;
        cli->is_admin = 0;
        cli->is_muted = 0;
        cli->room[0] = '\0';
        // 초기 username은 임시로 "Guest[소켓번호]"로 설정 (로그인 후 실제 ID로 업데이트)
        snprintf(cli->username, ID_LEN - 1, "Guest%d", clnt_sock);
        cli->username[ID_LEN - 1] = '\0';
        // 초기 name도 임시로 설정
        snprintf(cli->name, ID_LEN - 1, "게스트%d", clnt_sock);
        cli->name[ID_LEN - 1] = '\0';


        clients[idx] = cli; // 포인터를 배열에 저장
        client_count++;

        // chat_session 스레드 생성 (Client* 포인터를 인자로 전달)
        pthread_create(&cli->tid, NULL, chat_session, (void*)cli);
        pthread_detach(cli->tid); // 스레드 자동 소멸 설정

        pthread_mutex_unlock(&mutex);

        printf("[서버] 새로운 클라이언트 접속 시도: Guest%d (%s:%d), 현재 접속자: %d명\n",
               clnt_sock, inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port), client_count);
    }
    
    // 서버 종료 처리
    close(serv_sock);
    mysql_close(conn);
    mysql_library_end();
    return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

미리보기용

 

eull
eull
eull 님의 블로그 입니다.