이지우 - 관리자 저작권체험교실 서약서 일괄 다운로드 추가

This commit is contained in:
jiwoo 2024-01-17 12:31:19 +09:00
parent 57158baf69
commit 52a3f301e1
11 changed files with 279 additions and 138 deletions

View File

@ -105,4 +105,6 @@ public interface VEEduMIXService {
List<VEEduAplctVO> selectCndtnList(VEEduAplctVO paramVO); List<VEEduAplctVO> selectCndtnList(VEEduAplctVO paramVO);
List<VEEduAplctVO> selectAdultRsltRprtList(VEEduAplctVO paramVO) throws Exception; List<VEEduAplctVO> selectAdultRsltRprtList(VEEduAplctVO paramVO) throws Exception;
List<VEEduAplctVO> selectExprnAtchFileDownList(VEEduAplctVO paramVO) throws Exception;
} }

View File

@ -261,5 +261,9 @@ public class VEEduMIXDAO extends EgovAbstractDAO {
return tlist; return tlist;
} }
public List<VEEduAplctVO> selectExprnAtchFileDownList(VEEduAplctVO paramVO) throws Exception {
@SuppressWarnings("unchecked")
List<VEEduAplctVO> tlist = (List<VEEduAplctVO>) list("VEEduMIXDAO.selectExprnAtchFileDownList", paramVO);
return tlist;
}
} }

View File

@ -232,4 +232,8 @@ public class VEEduMIXServiceImpl implements VEEduMIXService {
public List<VEEduAplctVO> selectAdultRsltRprtList(VEEduAplctVO paramVO) throws Exception{ public List<VEEduAplctVO> selectAdultRsltRprtList(VEEduAplctVO paramVO) throws Exception{
return vEEduMIXDAO.selectAdultRsltRprtList(paramVO); return vEEduMIXDAO.selectAdultRsltRprtList(paramVO);
} }
public List<VEEduAplctVO> selectExprnAtchFileDownList(VEEduAplctVO paramVO) throws Exception{
return vEEduMIXDAO.selectExprnAtchFileDownList(paramVO);
}
} }

View File

@ -11,9 +11,10 @@ import java.io.PrintWriter;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -32,7 +33,6 @@ import org.springframework.ui.ModelMap;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
@ -525,6 +525,38 @@ String[] order = {
return modelAndView; return modelAndView;
} }
/**
* 저작권 체험교실 운영신청 목록 업로드 파일 체크
* @param model
* @return
* @throws Exception
*/
@RequestMapping(value = "oprtnAplctFileAllChkAjax.do")
public ModelAndView oprtnAplctFileAllChkAjax(VEEduAplctVO vEEduAplctVO
, HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("jsonView");
vEEduAplctVO.setEduAplctOrdList(Arrays.asList(vEEduAplctVO.getChk().split(",")));
List<VEEduAplctVO> vEEduAplctVOList = vEEduMIXService.selectExprnAtchFileDownList(vEEduAplctVO);
List<String> atchFileIdList = new ArrayList<String>();
vEEduAplctVOList.forEach( vo -> {
if(vo.getOathAtchFileId() != null) {
atchFileIdList.add(vo.getOathAtchFileId());
}
});
if(atchFileIdList.size() < 1) {
modelAndView.addObject("result", "fail");
modelAndView.addObject("msg", "첨부 파일이 없습니다.");
}else {
modelAndView.addObject("result", "success");
}
return modelAndView;
}
/** /**
* 저작권 체험교실 운영신청 목록 업로드 파일 일괄 다운로드 * 저작권 체험교실 운영신청 목록 업로드 파일 일괄 다운로드
* @param model * @param model
@ -532,70 +564,37 @@ String[] order = {
* @throws Exception * @throws Exception
*/ */
@RequestMapping(value = "oprtnAplctFileAllDownLoad.do") @RequestMapping(value = "oprtnAplctFileAllDownLoad.do")
public void oprtnAplctFileAllDownLoad(@RequestParam Map<String, Object> commandMap public void oprtnAplctFileAllDownLoad(VEEduAplctVO vEEduAplctVO
, HttpServletRequest request, HttpServletResponse response) throws Exception { , HttpServletRequest request, HttpServletResponse response) throws Exception {
//파일 SN을 리스트에 담기
vEEduAplctVO.setEduAplctOrdList(Arrays.asList(vEEduAplctVO.getChk().split(",")));
List<VEEduAplctVO> vEEduAplctVOList = vEEduMIXService.selectExprnAtchFileDownList(vEEduAplctVO);
vEEduAplctVOList = egovCryptoUtil.decryptVEEduAplctVOList(vEEduAplctVOList);
//첨부파일있는 항목만 재배치
String orgnZipNm = "체험교실 신청서.zip";
String downloadType = "A";
//첨부파일있는 항목만 재배치
List<String> atchFileIdList = new ArrayList<String>(); List<String> atchFileIdList = new ArrayList<String>();
//파일 SN을 리스트에 담기 vEEduAplctVOList.forEach( vo -> {
List<String> atchFileSnList = new ArrayList<String>(); if(vo.getOathAtchFileId() != null) {
atchFileIdList.add(vo.getOathAtchFileId());
//split을 이용해 아이디를 각자 배열에 담기
String[] splitIdStr = commandMap.get("atchFileId").toString().split(",");
String[] splitSnStr = commandMap.get("fileSn").toString().split(",");
//zip파일 이름
String orgnZipNm = commandMap.get("orgnZipNm").toString();
//downloadType (A:ID가 여러개고 fileSn이 1개인 경우 || B:ID는 하나이고 fileSn이 여러개인 경우)
String downloadType = commandMap.get("downloadType").toString();
String atchFileId = new String();
String fileSn = new String();
//ID가 여러개고 fileSn이 1개인 경우
if("A".equals(downloadType)) {
fileSn = "0";
for(int i=0; i<splitIdStr.length; i++) {
atchFileIdList.add(splitIdStr[i]);
} }
} });
//ID는 하나이고 fileSn이 여러개인 경우 List<VEEduAplctVO> fileYEduList = new ArrayList<VEEduAplctVO>();
if("B".equals(downloadType)) { vEEduAplctVOList.forEach( vo -> {
atchFileId = splitIdStr[0]; if(vo.getOathAtchFileId() != null) {
for(int i=0; i<splitSnStr.length; i++) { fileYEduList.add(vo);
atchFileSnList.add(splitSnStr[i]);
} }
} });
FileVO fileVO = new FileVO(); FileVO fileVO = new FileVO();
fileVO.setDownloadType(downloadType); fileVO.setDownloadType(downloadType);
if("A".equals(downloadType)) { fileVO.setAtchFileIdList(atchFileIdList);
fileVO.setAtchFileIdList(atchFileIdList);
fileVO.setFileSn(fileSn);
} else if("B".equals(downloadType)) {
fileVO.setAtchFileId(atchFileId);
fileVO.setAtchFileSnList(atchFileSnList);
}
List<FileVO> fvoList = fileService.selectZipFileList(fileVO); // 해당 기능에 맞게 파일 조회 List<FileVO> fvoList = fileService.selectZipFileList(fileVO); // 해당 기능에 맞게 파일 조회
if(fvoList.size() == 0){
response.setContentType("application/x-msdownload");
PrintWriter printwriter = response.getWriter();
printwriter.println("<html>");
printwriter.println("<br><br><br><h2>Could not get file name:<br></h2>");
printwriter.println("<br><br><br><center><h3><a href='javascript: history.go(-1)'>Back</a></h3></center>");
printwriter.println("<br><br><br>&copy; webAccess");
printwriter.println("</html>");
printwriter.flush();
printwriter.close();
return ;
}
// buffer size
int size = 1024; int size = 1024;
byte[] buf = new byte[size]; byte[] buf = new byte[size];
@ -604,39 +603,66 @@ String[] order = {
ZipArchiveOutputStream zos = null; ZipArchiveOutputStream zos = null;
BufferedInputStream bis = null; BufferedInputStream bis = null;
int fileCnt = 0;
try { try {
System.out.println("outZipNm : "+ outZipNm);
// Zip 파일생성 // Zip 파일생성
zos = new ZipArchiveOutputStream(new BufferedOutputStream(new FileOutputStream(outZipNm))); zos = new ZipArchiveOutputStream(new BufferedOutputStream(new FileOutputStream(outZipNm)));
for ( FileVO vo : fvoList ){ Iterator<FileVO> fvoIterator = fvoList.iterator();
Iterator<VEEduAplctVO> fileYEduIterator = fileYEduList.iterator();
while (fvoIterator.hasNext() && fileYEduIterator.hasNext() ){
FileVO vo = fvoIterator.next();
VEEduAplctVO eduVO = fileYEduIterator.next();
zos.setEncoding("UTF-8"); zos.setEncoding("UTF-8");
// Create a file object
File file = new File(vo.getFileStreCours() + "/" + vo.getStreFileNm());
// 1. check if the file exists or not
boolean isExists = file.exists();
if(isExists) {
System.out.println("getStreFileNm() " + vo.getStreFileNm());
System.out.println("I find the existFile.txt");
fileCnt++;
} else {
continue;
}
/*String renamedFileName = generateRenamedFileName(vo.getOrignlFileNm());*/
String renamedFileName = eduVO.getScholInsttNm() + "_" + eduVO.getChrgNm() + "_신청서."+ vo.getFileExtsn();
vo.setOrignlFileNm(renamedFileName);
//buffer에 해당파일의 stream을 입력한다. //buffer에 해당파일의 stream을 입력한다.
fis = new FileInputStream(vo.getFileStreCours() + "/" + vo.getStreFileNm()); fis = new FileInputStream(vo.getFileStreCours() + "/" + vo.getStreFileNm());
bis = new BufferedInputStream(fis,size); bis = new BufferedInputStream(fis,size);
//zip에 넣을 다음 entry 가져온다. //zip에 넣을 다음 entry 가져온다.
zos.putArchiveEntry(new ZipArchiveEntry(vo.getOrignlFileNm())); zos.putArchiveEntry(new ZipArchiveEntry(vo.getOrignlFileNm()));
//준비된 버퍼에서 집출력스트림으로 write 한다. //준비된 버퍼에서 집출력스트림으로 write 한다.
int len; int len;
while((len = bis.read(buf,0,size)) != -1) zos.write(buf,0,len); while((len = bis.read(buf,0,size)) != -1) zos.write(buf,0,len);
bis.close(); bis.close();
fis.close(); fis.close();
zos.closeArchiveEntry(); zos.closeArchiveEntry();
}
zos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally{
if( zos != null ) zos.close();
if( fis != null ) fis.close();
if( bis != null ) bis.close();
} }
zos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally{
if( zos != null ) zos.close();
if( fis != null ) fis.close();
if( bis != null ) bis.close();
}
File uFile = new File(fvoList.get(0).getFileStreCours(), orgnZipNm); File uFile = new File(fvoList.get(0).getFileStreCours(), orgnZipNm);
long fSize = uFile.length(); long fSize = uFile.length();
@ -690,57 +716,6 @@ String[] order = {
} }
} }
private void setDisposition(String filename, HttpServletRequest request, HttpServletResponse response) throws Exception {
String browser = getBrowser(request);
String dispositionPrefix = "attachment; filename=";
String encodedFilename = null;
if (browser.equals("MSIE")) {
encodedFilename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", "%20");
} else if (browser.equals("Trident")) { // IE11 문자열 깨짐 방지
encodedFilename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", "%20");
} else if (browser.equals("Firefox")) {
encodedFilename = "\"" + new String(filename.getBytes("UTF-8"), "8859_1") + "\"";
} else if (browser.equals("Opera")) {
encodedFilename = "\"" + new String(filename.getBytes("UTF-8"), "8859_1") + "\"";
} else if (browser.equals("Chrome")) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < filename.length(); i++) {
char c = filename.charAt(i);
if (c > '~') {
sb.append(URLEncoder.encode("" + c, "UTF-8"));
} else {
sb.append(c);
}
}
encodedFilename = sb.toString();
} else {
//throw new RuntimeException("Not supported browser");
throw new IOException("Not supported browser");
}
// response.setHeader("Content-Disposition", dispositionPrefix + encodedFilename); // 파일명에 콤마 포함시 오류
response.setHeader("Content-Disposition", dispositionPrefix + "\"" + encodedFilename + "\"");
if ("Opera".equals(browser)) {
response.setContentType("application/octet-stream;charset=UTF-8");
}
}
private String getBrowser(HttpServletRequest request) {
String header = request.getHeader("User-Agent");
if (header.indexOf("MSIE") > -1) {
return "MSIE";
} else if (header.indexOf("Trident") > -1) { // IE11 문자열 깨짐 방지
return "Trident";
} else if (header.indexOf("Chrome") > -1) {
return "Chrome";
} else if (header.indexOf("Opera") > -1) {
return "Opera";
}
return "Firefox";
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// //
@ -794,4 +769,55 @@ String[] order = {
return p_paginationInfo; return p_paginationInfo;
} }
private void setDisposition(String filename, HttpServletRequest request, HttpServletResponse response) throws Exception {
String browser = getBrowser(request);
String dispositionPrefix = "attachment; filename=";
String encodedFilename = null;
if (browser.equals("MSIE")) {
encodedFilename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", "%20");
} else if (browser.equals("Trident")) { // IE11 문자열 깨짐 방지
encodedFilename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", "%20");
} else if (browser.equals("Firefox")) {
encodedFilename = "\"" + new String(filename.getBytes("UTF-8"), "8859_1") + "\"";
} else if (browser.equals("Opera")) {
encodedFilename = "\"" + new String(filename.getBytes("UTF-8"), "8859_1") + "\"";
} else if (browser.equals("Chrome")) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < filename.length(); i++) {
char c = filename.charAt(i);
if (c > '~') {
sb.append(URLEncoder.encode("" + c, "UTF-8"));
} else {
sb.append(c);
}
}
encodedFilename = sb.toString();
} else {
//throw new RuntimeException("Not supported browser");
throw new IOException("Not supported browser");
}
// response.setHeader("Content-Disposition", dispositionPrefix + encodedFilename); // 파일명에 콤마 포함시 오류
response.setHeader("Content-Disposition", dispositionPrefix + "\"" + encodedFilename + "\"");
if ("Opera".equals(browser)) {
response.setContentType("application/octet-stream;charset=UTF-8");
}
}
private String getBrowser(HttpServletRequest request) {
String header = request.getHeader("User-Agent");
if (header.indexOf("MSIE") > -1) {
return "MSIE";
} else if (header.indexOf("Trident") > -1) { // IE11 문자열 깨짐 방지
return "Trident";
} else if (header.indexOf("Chrome") > -1) {
return "Chrome";
} else if (header.indexOf("Opera") > -1) {
return "Opera";
}
return "Firefox";
}
} }

View File

@ -8047,4 +8047,19 @@ VALUES
a.LCTR_DIV_CD = '20' a.LCTR_DIV_CD = '20'
<iterate open="(" close=")" conjunction="," property="rsltList" prepend="AND a.edu_aplct_ord IN" > #rsltList[]#</iterate> <iterate open="(" close=")" conjunction="," property="rsltList" prepend="AND a.edu_aplct_ord IN" > #rsltList[]#</iterate>
</select> </select>
<select id="VEEduMIXDAO.selectExprnAtchFileDownList" parameterClass="VEEduAplctVO" resultClass="VEEduAplctVO">
/* 임시.*NOT_SQL_LOG.* VEEduMIXDAO.selectExprnAtchFileDownList */
SELECT
A.edu_aplct_ord AS eduAplctOrd,
A.schol_instt_nm AS scholInsttNm,
A.chrg_nm AS chrgNm,
A.oath_atch_file_id AS oathAtchFileId
FROM VE_EDU_APLCT A
WHERE A.edu_aplct_ord IN
<iterate property="eduAplctOrdList" open="(" close=")" conjunction=",">
#eduAplctOrdList[]#
</iterate>
</select>
</sqlMap> </sqlMap>

View File

@ -309,6 +309,18 @@
</td> </td>
</tr> </tr>
<c:if test="${!empty info.oathAtchFileId}">
<tr>
<th scope="row">
<p>서약서</p>
</th>
<td>
<c:import url="/cmm/fms/selectBBSFileInfs.do" charEncoding="utf-8">
<c:param name="param_atchFileId" value="${info.oathAtchFileId}" />
</c:import>
</td>
</tr>
</c:if>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -140,12 +140,40 @@ input:read-only {
} }
} }
function fileChk() {
var chkLen = $(listForm).find("input[name=chk]:checked").length;
if(chkLen ==0){
alert("선택된 항목이 없습니다.");
return;
}
var downForm = new FormData(document.getElementById("listForm"));
$.ajax({
type:"POST",
url: "<c:url value='/kccadr/oprtn/cpyrgExprnClsrm/oprtnAplctFileAllChkAjax.do'/>",
data: downForm,
dataType:'json',
async: false,
processData: false,
contentType: false,
cache: false,
success:function(returnData){
console.log('returnData : ', returnData);
if(returnData.result == 'fail'){
alert(returnData.msg);
}else{
fileDownLoad();
}
},
error:function(request , status, error){
alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);
}
});
}
function fileDownLoad() { function fileDownLoad() {
/* alert("개발전"); var downForm = document.listForm;
return; */ downForm.action = "<c:url value='/kccadr/oprtn/cpyrgExprnClsrm/oprtnAplctFileAllDownLoad.do'/>";
var listForm = document.listForm; downForm.submit();
listForm.action = "<c:url value='/kccadr/oprtn/cpyrgExprnClsrm/oprtnAplctFileAllDownLoad.do'/>";
listForm.submit();
} }
function fncCreate() { function fncCreate() {
@ -167,6 +195,8 @@ input:read-only {
value="<c:out value="${vEEduAplctVO.searchSortOrd}" />" /> value="<c:out value="${vEEduAplctVO.searchSortOrd}" />" />
<input type="hidden" name="eduAplctOrd" value="" /> <input type="hidden" name="eduAplctOrd" value="" />
<input type="hidden" name="aprvlCd" id="aprvlCd" value="" /> <input type="hidden" name="aprvlCd" id="aprvlCd" value="" />
<input type="hidden" name="eduAplctOrdList" id="eduAplctOrdList" value="" />
<div class="cont_wrap"> <div class="cont_wrap">
<div class="box"> <div class="box">
<!-- cont_tit --> <!-- cont_tit -->
@ -305,7 +335,7 @@ input:read-only {
<c:if test="${vEEduAplctVO.pageUnit == '100'}">selected</c:if>>100줄</option> <c:if test="${vEEduAplctVO.pageUnit == '100'}">selected</c:if>>100줄</option>
</select> </select>
<button type="button" class="btn_type06" style="height:40px; border:1px solid #3a72db; font-size:16px; border-radius:5px; vertical-align:middle;" <button type="button" class="btn_type06" style="height:40px; border:1px solid #3a72db; font-size:16px; border-radius:5px; vertical-align:middle;"
onclick="fileDownLoad();">첨부파일 다운로드</button> onclick="fileChk();">첨부파일 다운로드</button>
<button type="button" class="btn_down_excel" <button type="button" class="btn_down_excel"
onclick="excelDownLoad();">엑셀 다운로드</button> onclick="excelDownLoad();">엑셀 다운로드</button>
</div> </div>

View File

@ -332,6 +332,18 @@
</td> </td>
</tr> </tr>
<c:if test="${!empty info.oathAtchFileId}">
<tr>
<th scope="row">
<p>서약서</p>
</th>
<td>
<c:import url="/cmm/fms/selectBBSFileInfs.do" charEncoding="utf-8">
<c:param name="param_atchFileId" value="${info.oathAtchFileId}" />
</c:import>
</td>
</tr>
</c:if>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -131,6 +131,41 @@
, $("#listForm") , $("#listForm")
); );
} }
function fileChk() {
var chkLen = $(listForm).find("input[name=chk]:checked").length;
if(chkLen ==0){
alert("선택된 항목이 없습니다.");
return;
}
var downForm = new FormData(document.getElementById("listForm"));
$.ajax({
type:"POST",
url: "<c:url value='/kccadr/oprtn/cpyrgExprnClsrm/oprtnAplctFileAllChkAjax.do'/>",
data: downForm,
dataType:'json',
async: false,
processData: false,
contentType: false,
cache: false,
success:function(returnData){
console.log('returnData : ', returnData);
if(returnData.result == 'fail'){
alert(returnData.msg);
}else{
fileDownLoad();
}
},
error:function(request , status, error){
alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error);
}
});
}
function fileDownLoad() {
var downForm = document.listForm;
downForm.action = "<c:url value='/kccadr/oprtn/cpyrgExprnClsrm/oprtnAplctFileAllDownLoad.do'/>";
downForm.submit();
}
</script> </script>
<title>교육과정관리</title> <title>교육과정관리</title>
</head> </head>
@ -276,6 +311,7 @@
<option value='30' <c:if test="${adjReqMgrVO.pageUnit == '30'}">selected</c:if>>30줄</option> <option value='30' <c:if test="${adjReqMgrVO.pageUnit == '30'}">selected</c:if>>30줄</option>
<option value='100' <c:if test="${adjReqMgrVO.pageUnit == '100'}">selected</c:if>>100줄</option> <option value='100' <c:if test="${adjReqMgrVO.pageUnit == '100'}">selected</c:if>>100줄</option>
</select> </select>
<button type="button" class="btn_type06" style="height:40px; border:1px solid #3a72db; font-size:16px; border-radius:5px; vertical-align:middle;" onclick="fileChk();">첨부파일 다운로드</button>
<button type="button" class="btn_down_excel" onclick="excelDownLoad();">엑셀 다운로드</button> <button type="button" class="btn_down_excel" onclick="excelDownLoad();">엑셀 다운로드</button>
</div> </div>
</div> </div>

View File

@ -143,7 +143,7 @@
<li> <li>
<div class="wrap"> <div class="wrap">
<div class="title"> <div class="title">
<p><img src="/offedu/visitEdu/usr/publish/images/content/mypage_icon03.png" alt="체험교실 마이페이지 아이콘"> 찾교(체험교실)</p> <p><img src="/offedu/visitEdu/usr/publish/images/content/mypage_icon03.png" alt="체험교실 마이페이지 아이콘">체험교실</p>
</div> </div>
<div class="inner_text" style="text-align: left;"> <div class="inner_text" style="text-align: left;">
<a href="#" onclick="fn_goExprnListForm(20)"> <a href="#" onclick="fn_goExprnListForm(20)">

View File

@ -66,7 +66,7 @@
<th scope="row"> <th scope="row">
<p>교육일정</p> <p>교육일정</p>
</th> </th>
<td>연중</td> <td>(신청) 2월말~3월초, (운영) 3월~11월</td>
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">