본문 바로가기

Common Gateway Interface/Perl

[옛 강좌] 24. Perlprog - BBS 3

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

 이 게시물은 지금은 폐쇄되어 접속되지 않는 Kim Young Soo(http://hours.interpia98.net/~unisoo/)님의 웹 사이트에 2001년경 게시된 내용을 바탕으로 오늘날 웹 환경에 맞게 내용을 덧붙였습니다.

Perlprog - BBS 3

Description

게시판 만들기 3 - 목록을 화면에 출력


BBS 만들기 3

 이번엔 저번에 이어서 저장된 인덱스파일에서 자료를 불러서 화면에 목록을 출력하는 부분을 하겠습니다.

추가사항(add)

 그러기 전에.... 지난번에 제가 말씀드리지 않은것이 있어서여.... 그거부터 말씀드릴께여 ... 너그러이....

 먼저 board_write.cgi 부분에서여... 이름을 쓰게되어있는 태그에 최대길이가 80으로 잡혀 있을 겁니다. 그걸 여러분들 사정에 맞게 줄이시구여...

 저는 글이 입력되었는지... 길이를 넘어서는지.... 굳이 cgi에서 체크 하지 않도록 했습 니다. 지금 까지 cgi에서 확인했던 건, 우리가 분리자로 쓰는 '::'의 체크 여붑니다.

 둘째... basic.pl 부분의 parse_input() 함수에

$value =~ s/</&lt/g;
$value =~ s/>/&gt/g;

이와 같은 두 줄을 넣어주세요. 이건 사용자가 태그를 입력하였을 때, 기호로 바꾸어 줍니다. 그렇지 않으면 브라우저 출력 시 태그로 인식하게 되죠. 아예 없앨까 생각을 했었는데, <FILE> 과같은 부분이 프로그램상에 자주 나오기 때문에 태그로 되어있는 부분을 없앨 수는 없고 해서, 기호로 바꾸어 주었습니다.

 그리고 open() || die "...'; 이런 식으로 파일을 열 때 기냥 프로그램을 종료시켰었는데, 이렇게 하면 에러가 났을경우 왜 에러가 났는지 알수가 없으니깐, basic.pl에 error_message() 함수를 추가 하겠습니다.

error_message()

sub error_message
{
	my ($message) = @_;
	
	print "<html><body><font size=2 color=red><b>\n";
	&print_content;
	print "$message</b></font></body></html>\n";
}
						

위와 같이 하겠습니다. 출력형식은 여러분들이 조절하시면 됩니다.

 그래서 파일을 여는 부분의 코드를 open(FILE, $idx_file) || die &error_message("$idx_file을 열수 없습니다."); 이런식으로 해서 잘못되었을 경우 어느 파일에서 왜 에러가 났는지 알게끔 했습니다.

view_list.cgi - 목록 출력

 그럼 view_list.cgi 를 시작하겠습니다. 저번에 어느분이 게시판에 질문을 하셨었는데... 절대값을 주고, 삭제를 하게되면 번호가 이상하게 되죠. 목록을 보여줄 때, 앞에 붙여주는 번호는 저장이 되어지는 고유인덱스 번호 로 처리하지 않습니다.

 가상 번호로 처리를 하게 되죠. 이 가상번호와 실제의 인덱스 번호를 연결시키게 됩니다. 그러면 몇 개가 어디서 지워지더라도 목록 상의 번호는 중간의 끊김이 없이 나오게 됩니다.

 실제로 제목을 클릭해서 하나의 파일을 열 때도 목록에 보여지는 번호의 파일을 열게되는 것이 아니고, 그 번호가 참조하는 실제의 파일을 열게 되는 거지요.

 답변을 해서 인덱스 중간중간에 새로운 인덱스가 생성되더라도 마찬가지 입니다. 그럼 소스를 보면서....

#! /usr/local/bin/perl

require "./basic.pl";
require "./board.conf";

&print_header;

$query_data = $ENV{'QUERY_STRING'};
					

위의 코드를 이해하지 몬하신다면..... 앞부분부터 차근차근.....

$total_idx = &get_max_number($idx_file);

 인덱스 파일의 총 데이터 수를 불러 오게 됩니다. 그래야 목록상의 번호를 차례로 매길 수 있겠죠. 아래는 get_max_number() 함수의 원형입니다.

get_max_number

sub get_max_number
{
	my ($filename) = @_;
	my (@data, $max_number);
	
	open (FILE, "$filename") || die &error_message("$filename을 열수 없습니다.");
	@data = <FILE>;
	close (FILE);
	
	$max_number = $#data + 1;
	return $max_number;
}
						

 위에서 보는바와 같습니다. $#변수는 배열의 마지막 원소의 번호를 반환합니다. 근데 마지막에 1을 더한 이유는, 프로그램상에서 배열의 개수는 0부터 시작을 하잖아요. 데이터가 21가 있다면 마지막 배열에서 마지막 원소의 번호는 20이 되죠. 실제로는 21개. 그러니 우리는 거기에 1을 더해서 계산을 하게 됩니다. 그리고 그 수를 반환합니다.

if (!$query_data)
{
	$virtual_number = $total_idx;
	$start_number = 0;
	$last_number = $list_number;
}
else
{
	&parse_data($query_data, *view_num);
	
	$virtual_number = $start_number = $view_num{'move'};
	$start_number = $total_idx - $start_number;
	$last_number = $list_number;
}
						

 우리가 add_data.cgi의 마지막 부분엔 아무런 인자값을 넘기지 않고 view_list.cgi를 호출 하게 됩니다. 이 때 우리가 사용할 기본적이 변수들을 정하게 되죠. 즉 처음 리스트의 필요한 변수들을 초기화 하게 됩니다.

 그러나.... 다음 페이지나 하나의 파일을 본 후 목록으로 돌아올 땐 인자가 넘어오게 되죠. 그땐 그 변수값에 맞춰서 변수들을 초기화하게 됩니다.

 $virtual_number는 우리가 목록 상에서 번호를 매길 가상 번호입니다. 이 번호는 실제의 인덱스 번호와 차이가 나죠.

 $start_number는 목록을 보여줄 때 몇 번부터 보여줄 것인가 입니다. 이 시작점의 계산은 기냥 하면 안되죠. 배열에 저장되는 건 0부터구, 우리가 보여줄 때의 계산은 마지막 번호부터 하게되죠. 이유? 번호가 큰 게 위로 올라오게 되잖아여. 그러니 배열과는 거꾸로 되죠. 그래서 총 데이터 개수에서 가상번호를 빼준만큼이, 진짜로 우리가 보고자 하는 데이터의 번호가 됩니다.

 $last_number는 $start_number부터 .... $last_number까지 보여주게 됩니다. $list_nubmer는 board.conf에 정의가 되어있죠.

 그리고... &parse_data() 함수가 있는데, 기능은 &parse_input()과 같습니다. 하지만 서로 다르게 써야하는 경우가 있어서 따로 만들었습니다. 함수 원형은 다음과 같습니다.

parse_data()

sub parse_data
{
	my ($temp) = $_[0];
	local (*parse_data) = $_[1];
	
	my (@pairs, $key, $value);
	
	@pairs = split(/&/, $temp);
	
	foreach (@pairs)
	{
		($key, $value) = split(/=/, $_);
		$value =~ tr/+/ /;
		$value =~ s/%(..)/pack("c", hex($1))/ge;
		$parse_data{$key} = $value;
	}
	
	return 1;
}
						

 parse_input() 과 다른 점이라면 코드가 몇 개 빠졌다는거죠.... 이건 제가 미처 생각지 못한 부분에서 문제가 발생을 했기 때문에 이렇게 됐습니다. -.-;

 저는 view_list.cgi를 오직 목록을 보여주는 기능만을 하게 했기 때문에, 실제로 넘어오는 변수는 하나만 있으면 됩니다. 즉....

 몇 번째 목록부터 보여줄거냐.... 하는 그 변수만 있으면 됩니다. 그 이후의 나머지 과정은 아래의 프로그램이 담당하게 됩니다.

if ($total_idx == 0)
{
	&html_header($total_idx);
	print "
		<tr>
			<td width=550 colspan=5 height=35 align=center bgcolor=#defaf8><font size=2 color=#568c97><b>등록된 글이 없습니다.</b></font></td>
		</tr>
		<tr>
			<td colspan=5 align=left>
				<a href=\"./home.html\" onMouseOver=\"window.status=('Home'); return true;\"><img src=./icons/i_home.gif border=0 alt=\"홈으로\"></a>
				<a href=\"./board_write.cgi\" onMouseOver=\"window.status=('Write'); return true;\"><img src=./icons/i_write.gif border=0 alt=\"글쓰기\"></a>
			</td>
		</tr>
	</table>\n";
}
						

 음.. 만약에 인덱스 파일에 데이타가 하나도 없다면..... 등록된 글이 없다고 알려야져... 그리고 마우스로 링크에 갖다 댈 때... 상태표시줄에 주소줄이 써지는건 별로 안 이뿌니깐.. 자바스크립트를 사용해서 보기좋은 글을 출력하도록 했습니다.

 그리고 html_header()함수는 계속 쓰이는 거니까 view_list.cgi안에 서브루틴으로 정의해서 넣습니다.

html_header()

sub html_header
{
	my ($total_idx_num) = @_;
	print "
		<html>
			<head>
				<title>CGI board</title>
				<style type=\"text/css\">
					A:link          { color:#69934c; text-decoration : none}
					A:visited       { color:#69934c; text-decoration : none}
					A:hover         { color:red; text-decoration : underline }
				</style>
			</head>
			<body bgcolor=white>
		\n";
		
		&print_content;
		
		print "
			<table width=550 border=0>
				<tr>
					<td colspan=2 bgcolor=white align=left><font size=2 color=blue>총게시물 : $total_idx_num개</font></td>
					<td colspan=3 bgcolor=white align=right><font size=2 color=blue></font></td>
				</tr>
				<tr>
					<td width=55 height=35 align=center bgcolor=#defaf8><font size=2 color=#568c97><b>번호</b></font></td>
					<td width=70 align=center bgcolor=#defaf8><font size=2 color=#568c97><b>이름</b></font></td>
					<td width=300 align=center bgcolor=#defaf8><font size=2 color=#568c97><b>제 목</b></font></td>
					<td width=35 align=center bgcolor=#defaf8><font size=2 color=#568c97><b>조 회</b></font></td>
					<td width=90 align=center bgcolor=#defaf8><font size=2 color=#568c97><b>날 짜</b></font></td>
				</tr>
			\n";
}
						

 이 함수는 view_list.cgi 안에 정의가 된겁니다.

이전페이지, 다음페이지(next, prev)

 그렇다면 데이터가 있다면? 무작정 출력할 수는 없죠. board.conf에 보여주는 목록 수를 정의 했었죠. 데이터가 그 목록 수 보다 작다면, 넘는다면.... 넘었을 때 다음 목록과 이전목록에 링크를 없애는 문제... 등등등.... 이 부분을 꼴똘이 생각해본결과, 나의 짧은 머리로는 총 4개의 경우가 생기더군여...

  1. 이전, 다음 목록이 둘다 안눌려 지는 경우...
  2. 이전만 눌려지는 경우...
  3. 다음만 눌려지는 경우...
  4. 이전, 다음 둘다 눌려 지는 경우....

 여기서 더 말씀드릴 건... 대부분의 게시판들이 page 이동이 가능하게 되어 있습니다. 저는 이 부분을 뺐는데요... 그 이유는...

 데이터가 23개가 있습니다. 목록 수는 10개. 그렇다면 페이지는 총 3 페이지가 되겠죠.. 첫 페이지가 23번부터 14번까지... 둘째 페이지가 13번부터 4번까지, 마지막이 3번부터 1번까지겠죠... 근데 제가 15번을 클릭해서 데이터를 읽고 그 곳에서 다음글 다음글 눌러서 2번까지 이동을 했습니다. 그리고 목록을 눌렀을 땐.. 처음 페이지였던 1페이지가 출력이 되게 되죠.

 제가 한건 이런 방식이 아니고, 만약 위와 같이 했을경우, 목록을 누르면 2번부터 목록에 보여지게 됩니다. 즉 지금 본 데이터부터 밑으로 목록 수(예10)를 보여 주게 됩니다.

 이렇게 하니 페이지 계산이 애매하더군여... 총 3페이지라 하더라도 이런식으로 보여주게 되면 4페이지가 될 때가 있어서.... 그리고 페이지 이동에 대해 제가 편하다는 생각이 있는게 아니라서...

 그리고 한 가지 더 생각할 것은... 목록 수가 10이고 데이터가 9개라면, 당연 처음에는 위 경우 1번에 해당 되겠죠. 근데 4번 데이터를 읽고 목록 단추를 누르면 위의 1의 경우가 아니라 2의 경우가 됩니다.

 아래는 위 경우의 코드 입니다.

else
{
	&html_header($total_idx);
	
	if ($total_idx <= $list_number && $total_idx <= $virtual_number)
	{
		$case = 1;
		&read_data($start_number, $total_idx, $virtual_number);
		&html_bottom($case);
		print "</body></html>";
	}
	elsif ($virtual_number > 0 && $total_idx == $virtual_number)
	{
		$case = 2;
		&read_data($start_number, $last_number, $virtual_number);
		$prev_num = $virtual_number - $list_number;
		&html_bottom($case);
		print "</body></html>";
	}
	elsif ($virtual_number > 0 && $virtual_number > $list_number)
	{
		$case = 3;
		&read_data($start_number, $last_number, $virtual_number);
		$next_num = $virtual_number + $list_number;
		$prev_num = $virtual_number - $list_number;
		
		if ($next_num > $total_idx)
		{
			$next_num = $n_virtual_number = $total_idx;
		}
		
		&html_bottom($case);
		print "</body></html>";
	}
	else
	{
		$case = 4;
		&read_data($start_number, $virtual_number, $virtual_number);
		$next_num = $virtual_number + $list_number;
		
		if ($next_num > $total_idx)
		{
			$next_num = $n_virtual_number = $total_idx;
		}
		
		&html_bottom($case);
		print "</body></html>";
	}
}
					

 위의 제 말이 이해가 잘 안 되신다면 코드를 직접 보시면서 이해해 보세요. 위 코드는 1-4의 경우, 해당 데이터를 읽어서 화면에 뿌리고(&read_data), 다음과 이전목록을 상황에 맞게 표현하였습니다. 그리고 각각의 경우에 해당하는 코드를 화면에 뿌립니다(&html_bottom).

 참고로, 10개의 데이터 중 9번을 보았다면, 이전 단추가 눌리겠죠. 근데 보여줄 데이터 목록은 하나밖에 없음에도 불구하고, 따로 처리를 하지 않으면 빈 데이터 줄이 출력됩니다. 그래서 이전을 계산할때, 총 데이터 수를 넘을 때는 이전 변수값을 총 데이터 수로 설정합니다. 그러면 이런 현상은 없습니다. 1 이하의 데이터에서 이런 현상이 생기는거에 대해서는 read_data() 함수가 처리하게 됩니다. 즉 번호가 1이 되면 출력을 멈추면 되죠. 밑에 설명이.....

 그러면 각각을 이해하신다고 생각하고, 두 개의 함수(read_data(), html_bottom())에 대해서 살펴보겠습니다.

read_data()

sub read_data
{
	my ($start_num, $j, $count_number) = @_;
	my (@data_list, $idx, $name, $title, $count, $date, $i, $idx_length, $nbsp);
	my ($idx_nbsp, $nbsp_list, $k, $nbsp_count);
	
	local (*number);
	
	open (FILE, "$idx_file") || die &error_message("$idx_file을 열수 없습니다.");
	
	@data_list = <FILE>;
	
	for ($i=0; $i < $j; $i++)
	{
		($idx, $name, $title, $count, $date) = split (/::/, $data_list[$start_num + $i]);
		
		$number{$count_number} = $idx;
		$nbsp_count = split(/-/, $idx);
		$nbsp = "&nbsp";
		
		for ($k = 1; $k <= $nbsp_count; $k++)
		{
			$nbsp .= "&nbsp&nbsp";
		}
		
		if ($nbsp_count > 1)
		{
			$title = "└▷ ".$title;
		}
		
		# 이 부분은 해당 글의 답변이 있을때 처리하는 루틴입니다. 이건 그 때 가서 자세히 말씀드릴거구여, 지금은 그냥 참고 하시고, 각자 생각해 보세요. 
		
		if (length($title) > 30)
		{
			$title = substr($title, 0, 30)."...";
		}
		
		print "
			<tr>
				<td height=20 align=center bgcolor=#f0f9e1><font size=2 color=#69934c>$count_number</font></td>
				<td align=center bgcolor=#f0f9e1><font size=2 color=#2a6500>$name</font></td>
				<td bgcolor=#f0f9e1>$nbsp<a href=./board_view.cgi?real_number=$number{$count_number}&virtual_number=$count_number onMouseOver=\"window.status=('Read data $count_number'); return true;\"><font size=2>$title</font></a></td>
				<td bgcolor=#f0f9e1 align=center><font size=2 color=#2a6500>$count</font></td>
				<td align=center bgcolor=#f0f9e1><font size=2 color=#69934c>$date</font></td>
			</tr>\n";
			
		last if ($count_number == '1');
		$count_number--;
	}
	
	close (FILE);
}
						

 우선... read_data()는 3개의 변수를 넘겨받습니다. 시작번호, 총 데이터 개수, 가상번호. 그래서.....

 인덱스 파일을 열구 -> 시작번호에 해당하는 데이터부터 -> 끝나는 데이터까지 출력. 간단하게는 이렇습니다. 하지만 이런 과정중에 해쉬 배열인 %number에 가상번호와 진짜 인덱스 번호를 서로 연결시킵니다. 그래야 사용자가 해당 제목을 클릭했을 때, 정확한 데이터 파일을 불러올 수 있죠. 그래서 링크를 걸 때는 가상번호 -> 실제 인덱스번호(즉 이 번호는 실제 파일번호)를 링크시킵니다.

 그리고 가상번호를 하나씩 빼면서 출력하다가, 목록 수를 다 채웠거나, 다 채우기 전에 1번이다.. 그러면 루프를 빠져 나가게 됩니다. 그렇지 않다면 2번을 읽고 목록을 보게되면 1번 데이터 출력하고 빈 데이터 줄을 목록 수 만큼 채우게 됩니다. 이런 기현상을 방지하기 위해서....

 그리고 제목의 길이가 30자가 넘으면 그 이상은 '...'으로 처리 합니다.

 흠.... 이 과정이 이해가 되셨는지....

html_bottom()

sub html_bottom
{
	my ($case_num) = @_;
	
	print "
		<tr>
			<td colspan=5><hr width=100% size=2 color=#568c97 align=left></td>
		</tr>
		<tr>
			<td colspan=2>
				<a href=\"./board_pwd.cgi?mode=admin\" onMouseOver=\"window.status=('Admin'); return true;\">$ADMIN_IMAGE</a>
				<a href=$url onMouseOver=\"window.status=('Home'); return true;\">$HOME_IMAGE</a>
				<a href=\"./board_write.cgi\" onMouseOver=\"window.status=('Write'); return true;\">$WRITE_IMAGE</a><p>
			</td>
			<td align=center>
				<form action=./board_search.cgi method=post>
					<select name=search_list size=1>
						<option value=1 selected>제목</option>
						<option value=2>본문</option>
						<option value=3>이름</option>
					</select>
					<input type=text name=search size=15 maxlength=20>
					<input type=submit value=\"검색\" STYLE=\"background-color:aliceblue; border-width:1px; border-color:#376eda; border-style:solid;\">
				</form>
			</td>
			<td colspan=2 align=right>\n";
			
	if ($case == 1)
	{
		print "
			$NEXT_IMAGE
			<a href=./view_list.cgi onMouseOver=\"window.status=('List'); return true;\">$LIST_IMAGE</a>
			$PREV_IMAGE
			<p>
			</td>
			</tr>
			</table>\n";
	}
	elsif ($case == 2)
	{
		print "
			$NEXT_IMAGE
			<a href=./view_list.cgi onMouseOver=\"window.status=('List'); return true;\">$LIST_IMAGE</a>
			<a href=./view_list.cgi?move=$prev_num onMouseOver=\"window.status=('Preview Page'); return true;\">$PREV_IMAGE</a><p>
			</td>
			</tr>
			</table>\n";
	}
	elsif ($case == 3)
	{
		print "
			<a href=./view_list.cgi?move=$next_num onMouseOver=\"window.status=('Next Page'); return true;\">$NEXT_IMAGE</a>
			<a href=./view_list.cgi onMouseOver=\"window.status=('List'); return true;\">$LIST_IMAGE</a>
			<a href=./view_list.cgi?move=$prev_num onMouseOver=\"window.status=('Preview Page'); return true;\">$PREV_IMAGE</a><p>
			</td>
			</tr>
			</table>\n";
	}
	else
	{
		print "
			<a href=./view_list.cgi?move=$next_num onMouseOver=\"window.status=('Next Page'); return true;\">$NEXT_IMAGE</a>
			<a href=./view_list.cgi onMouseOver=\"window.status=('List'); return true;\">$LIST_IMAGE</a>
			$PREV_IMAGE<p>
			</td>
			</tr>
			</table>\n";
	}
}
						

 이 함수는 위에서 정의된 각각의 경우에 따라서 버튼을 출력하기만 하면 됩니다. 그리고 언제든지 리스트 단추를 누르면 처음 목록으로 돌아가게 됩니다.

 나오는 단추는 관리자 단추, 홈단추, 쓰기단추, 또 검색폼, 이전글, 리스트, 다음글 단추.. 요기서 사용이 안되는 건 검색폼과 관리자 밖에 없죠.

NOTES

 저장도 하고, 우리가 원하는 수만큼 목록을 보여줄 수도 있게 되었습니다. 이제 남은 것중 하나는 사용자가 읽고자 하는 데이터를 클릭했을 때 해당 데이터를 여는 일입니다.

 이 부분은 다음에 하겠습니다. 전체적인 소스는 이곳에 적지 않도록 하겠습니다. 각자 해보시구여, 잘안되면 게시판에 남겨 주시구여.


이 문서는 Perl 패키지내의 pod2html를 이용하여 만들었습니다. - Kim Young Soo